index.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import React, { ReactNode, useState, useCallback, useMemo } from 'react';
  2. import { createPortal } from 'react-dom';
  3. import { CSS as cssDndKit } from '@dnd-kit/utilities';
  4. import cls from 'classnames';
  5. import {
  6. closestCenter,
  7. DragOverlay,
  8. DndContext,
  9. MouseSensor,
  10. TouchSensor,
  11. useSensor,
  12. useSensors,
  13. KeyboardSensor,
  14. TraversalOrder,
  15. } from '@dnd-kit/core';
  16. import type {
  17. UniqueIdentifier,
  18. PointerActivationConstraint,
  19. CollisionDetection,
  20. } from '@dnd-kit/core';
  21. import {
  22. useSortable,
  23. SortableContext,
  24. rectSortingStrategy,
  25. sortableKeyboardCoordinates,
  26. } from '@dnd-kit/sortable';
  27. import type {
  28. SortingStrategy,
  29. AnimateLayoutChanges,
  30. NewIndexGetter,
  31. } from '@dnd-kit/sortable';
  32. import type { SortableTransition } from '@dnd-kit/sortable/dist/hooks/types';
  33. import { isNull } from 'lodash';
  34. const defaultPrefix = 'semi-sortable';
  35. interface OnSortEndProps {
  36. oldIndex: number;
  37. newIndex: number
  38. }
  39. export type OnSortEnd = (props: OnSortEndProps) => void;
  40. export interface RenderItemProps {
  41. id?: string | number;
  42. sortableHandle?: any;
  43. [x: string]: any
  44. }
  45. export interface SortableProps {
  46. onSortEnd?: OnSortEnd;
  47. // Set drag and drop trigger conditions
  48. activationConstraint?: PointerActivationConstraint;
  49. // Collision detection algorithm, for drag and drop sorting, use closestCenter to meet most scenarios
  50. collisionDetection?: CollisionDetection;
  51. // the dragged items,The content in items cannot be the number 0
  52. items?: any[];
  53. // Function that renders the item that is allowed to be dragged
  54. renderItem?: (props: RenderItemProps) => React.ReactNode;
  55. // Drag and drop strategy
  56. strategy?: SortingStrategy;
  57. // Whether to use a separate drag layer for items that move with the mouse
  58. useDragOverlay?: boolean;
  59. // A container for all elements that are allowed to be dragged
  60. container?: any;
  61. // Whether to change the size of the item being dragged
  62. adjustScale?: boolean;
  63. // Whether to use animation during dragging
  64. transition?: SortableTransition | null;
  65. // prefix
  66. prefix?: string;
  67. // The className of the item that moves with the mouse during the drag
  68. dragOverlayCls?: string
  69. }
  70. interface SortableItemProps {
  71. animateLayoutChanges?: AnimateLayoutChanges;
  72. getNewIndex?: NewIndexGetter;
  73. id: UniqueIdentifier;
  74. index: number;
  75. useDragOverlay?: boolean;
  76. renderItem?: (props: RenderItemProps) => ReactNode;
  77. prefix?: string;
  78. transition?: SortableTransition | null
  79. }
  80. function DefaultContainer(props) {
  81. return <div style={{ overflow: 'auto' }} {...props}></div>;
  82. }
  83. const defaultKeyBoardOptions = {
  84. coordinateGetter: sortableKeyboardCoordinates,
  85. };
  86. export function Sortable({
  87. items,
  88. onSortEnd,
  89. adjustScale,
  90. renderItem,
  91. transition,
  92. collisionDetection = closestCenter,
  93. strategy = rectSortingStrategy,
  94. useDragOverlay = true,
  95. dragOverlayCls,
  96. container: Container = DefaultContainer,
  97. prefix = defaultPrefix,
  98. }: SortableProps) {
  99. const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
  100. const sensors = useSensors(
  101. useSensor(MouseSensor),
  102. useSensor(TouchSensor),
  103. useSensor(KeyboardSensor, defaultKeyBoardOptions)
  104. );
  105. const getIndex = useCallback((id: UniqueIdentifier) => items.indexOf(id), [items]);
  106. const activeIndex = useMemo(() => activeId ? getIndex(activeId) : -1, [getIndex, activeId]);
  107. const onDragStart = useCallback(({ active }) => {
  108. if (!active) { return; }
  109. setActiveId(active.id);
  110. }, []);
  111. const onDragEnd = useCallback(({ over }) => {
  112. setActiveId(null);
  113. if (over) {
  114. const overIndex = getIndex(over.id);
  115. if (activeIndex !== overIndex) {
  116. onSortEnd({ oldIndex: activeIndex, newIndex: overIndex });
  117. }
  118. }
  119. }, [activeIndex, getIndex, onSortEnd]);
  120. const onDragCancel = useCallback(() => {
  121. setActiveId(null);
  122. }, []);
  123. return (
  124. <DndContext
  125. sensors={sensors}
  126. collisionDetection={collisionDetection}
  127. onDragStart={onDragStart}
  128. onDragEnd={onDragEnd}
  129. onDragCancel={onDragCancel}
  130. autoScroll={{ order: TraversalOrder.ReversedTreeOrder }}
  131. >
  132. <SortableContext items={items} strategy={strategy}>
  133. <Container>
  134. {items.map((value, index) => (
  135. <SortableItem
  136. key={value}
  137. id={value}
  138. index={index}
  139. renderItem={renderItem}
  140. useDragOverlay={useDragOverlay}
  141. prefix={prefix}
  142. transition={transition}
  143. />
  144. ))}
  145. </Container>
  146. </SortableContext>
  147. {useDragOverlay
  148. ? createPortal(
  149. <DragOverlay
  150. adjustScale={adjustScale}
  151. // Set zIndex in style to undefined to override the default zIndex in DragOverlay,
  152. // So that the zIndex of DragOverlay can be set by className
  153. style={{ zIndex: undefined }}
  154. className={dragOverlayCls}
  155. >
  156. {activeId ? (
  157. renderItem({
  158. id: activeId,
  159. sortableHandle: (WrapperComponent) => WrapperComponent
  160. })
  161. ) : null}
  162. </DragOverlay>,
  163. document.body
  164. )
  165. : null}
  166. </DndContext>
  167. );
  168. }
  169. export function SortableItem({
  170. animateLayoutChanges,
  171. id,
  172. renderItem,
  173. prefix,
  174. transition: animation,
  175. }: SortableItemProps) {
  176. const {
  177. listeners,
  178. setNodeRef,
  179. transform,
  180. transition,
  181. active,
  182. isOver,
  183. attributes,
  184. } = useSortable({
  185. id,
  186. animateLayoutChanges,
  187. transition: animation,
  188. });
  189. const sortableHandle = useCallback((WrapperComponent) => {
  190. // console.log('listeners', listeners);
  191. // 保证给出的接口的一致性,使用 span 包一层,保证用户能够通过同样的方式使用 handler
  192. // To ensure the consistency of the given interface
  193. // use a span package layer to ensure that users can use the handler in the same way
  194. // eslint-disable-next-line jsx-a11y/no-static-element-interactions
  195. return () => <span {...listeners} style={{ lineHeight: 0 }} onMouseDown={(e) => {
  196. listeners.onMouseDown(e);
  197. // 阻止onMousedown的事件传递,
  198. // 防止元素在点击后被卸载导致tooltip/popover的弹出层意外关闭
  199. // Prevent the onMousedown event from being delivered,
  200. // preventing the element from being unloaded after being clicked,
  201. // causing the tooltip/popover pop-up layer to close unexpectedly
  202. e.preventDefault();
  203. e.stopPropagation();
  204. }}
  205. ><WrapperComponent /></span>;
  206. }, [listeners]);
  207. const itemCls = cls(
  208. `${prefix}-sortable-item`,
  209. {
  210. [`${prefix}-sortable-item-over`]: isOver,
  211. [`${prefix}-sortable-item-active`]: active?.id === id,
  212. }
  213. );
  214. const wrapperStyle = useMemo(() => {
  215. return (!isNull(animation)) ? {
  216. transform: cssDndKit.Transform.toString({
  217. ...transform,
  218. scaleX: 1,
  219. scaleY: 1,
  220. }),
  221. transition: transition,
  222. } : undefined;
  223. }, [animation, transform, transition]);
  224. return <div
  225. ref={setNodeRef}
  226. style={wrapperStyle}
  227. className={itemCls}
  228. {...attributes}
  229. >
  230. {renderItem({ id, sortableHandle }) as JSX.Element}
  231. </div>;
  232. }