index.tsx 8.0 KB

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