123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- import React, { ReactNode, useState, useCallback, useMemo } from 'react';
- import { createPortal } from 'react-dom';
- import { CSS as cssDndKit } from '@dnd-kit/utilities';
- import cls from 'classnames';
- import {
- closestCenter,
- DragOverlay,
- DndContext,
- MouseSensor,
- TouchSensor,
- useSensor,
- useSensors,
- KeyboardSensor,
- TraversalOrder,
- } from '@dnd-kit/core';
- import type {
- UniqueIdentifier,
- PointerActivationConstraint,
- CollisionDetection,
- } from '@dnd-kit/core';
- import {
- useSortable,
- SortableContext,
- rectSortingStrategy,
- sortableKeyboardCoordinates,
- } from '@dnd-kit/sortable';
- import type {
- SortingStrategy,
- AnimateLayoutChanges,
- NewIndexGetter,
- } from '@dnd-kit/sortable';
- import type { SortableTransition } from '@dnd-kit/sortable/dist/hooks/types';
- import { isNull } from 'lodash';
- const defaultPrefix = 'semi-sortable';
- interface OnSortEndProps {
- oldIndex: number;
- newIndex: number
- }
- export type OnSortEnd = (props: OnSortEndProps) => void;
- export interface RenderItemProps {
- id?: string | number;
- sortableHandle?: any;
- [x: string]: any
- }
- export interface SortableProps {
- onSortEnd?: OnSortEnd;
- // Set drag and drop trigger conditions
- activationConstraint?: PointerActivationConstraint;
- // Collision detection algorithm, for drag and drop sorting, use closestCenter to meet most scenarios
- collisionDetection?: CollisionDetection;
- // the dragged items,The content in items cannot be the number 0
- items?: any[];
- // Function that renders the item that is allowed to be dragged
- renderItem?: (props: RenderItemProps) => React.ReactNode;
- // Drag and drop strategy
- strategy?: SortingStrategy;
- // Whether to use a separate drag layer for items that move with the mouse
- useDragOverlay?: boolean;
- // A container for all elements that are allowed to be dragged
- container?: any;
- // Whether to change the size of the item being dragged
- adjustScale?: boolean;
- // Whether to use animation during dragging
- transition?: SortableTransition | null;
- // prefix
- prefix?: string;
- // The className of the item that moves with the mouse during the drag
- dragOverlayCls?: string
- }
- interface SortableItemProps {
- animateLayoutChanges?: AnimateLayoutChanges;
- getNewIndex?: NewIndexGetter;
- id: UniqueIdentifier;
- index: number;
- useDragOverlay?: boolean;
- renderItem?: (props: RenderItemProps) => ReactNode;
- prefix?: string;
- transition?: SortableTransition | null
- }
- function DefaultContainer(props) {
- return <div style={{ overflow: 'auto' }} {...props}></div>;
- }
- const defaultKeyBoardOptions = {
- coordinateGetter: sortableKeyboardCoordinates,
- };
- export function Sortable({
- items,
- onSortEnd,
- adjustScale,
- renderItem,
- transition,
- collisionDetection = closestCenter,
- strategy = rectSortingStrategy,
- useDragOverlay = true,
- dragOverlayCls,
- container: Container = DefaultContainer,
- prefix = defaultPrefix,
- }: SortableProps) {
- const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
- const sensors = useSensors(
- useSensor(MouseSensor),
- useSensor(TouchSensor),
- useSensor(KeyboardSensor, defaultKeyBoardOptions)
- );
- const getIndex = useCallback((id: UniqueIdentifier) => items.indexOf(id), [items]);
- const activeIndex = useMemo(() => activeId ? getIndex(activeId) : -1, [getIndex, activeId]);
- const onDragStart = useCallback(({ active }) => {
- if (!active) { return; }
- setActiveId(active.id);
- }, []);
-
- const onDragEnd = useCallback(({ over }) => {
- setActiveId(null);
- if (over) {
- const overIndex = getIndex(over.id);
- if (activeIndex !== overIndex) {
- onSortEnd({ oldIndex: activeIndex, newIndex: overIndex });
- }
- }
- }, [activeIndex, getIndex, onSortEnd]);
-
- const onDragCancel = useCallback(() => {
- setActiveId(null);
- }, []);
- return (
- <DndContext
- sensors={sensors}
- collisionDetection={collisionDetection}
- onDragStart={onDragStart}
- onDragEnd={onDragEnd}
- onDragCancel={onDragCancel}
- autoScroll={{ order: TraversalOrder.ReversedTreeOrder }}
- >
- <SortableContext items={items} strategy={strategy}>
- <Container>
- {items.map((value, index) => (
- <SortableItem
- key={value}
- id={value}
- index={index}
- renderItem={renderItem}
- useDragOverlay={useDragOverlay}
- prefix={prefix}
- transition={transition}
- />
- ))}
- </Container>
- </SortableContext>
- {useDragOverlay
- ? createPortal(
- <DragOverlay
- adjustScale={adjustScale}
- // Set zIndex in style to undefined to override the default zIndex in DragOverlay,
- // So that the zIndex of DragOverlay can be set by className
- style={{ zIndex: undefined }}
- className={dragOverlayCls}
- >
- {activeId ? (
- renderItem({
- id: activeId,
- sortableHandle: (WrapperComponent) => WrapperComponent
- })
- ) : null}
- </DragOverlay>,
- document.body
- )
- : null}
- </DndContext>
- );
- }
- export function SortableItem({
- animateLayoutChanges,
- id,
- renderItem,
- prefix,
- transition: animation,
- }: SortableItemProps) {
- const {
- listeners,
- setNodeRef,
- transform,
- transition,
- active,
- isOver,
- attributes,
- } = useSortable({
- id,
- animateLayoutChanges,
- transition: animation,
- });
- const sortableHandle = useCallback((WrapperComponent) => {
- // console.log('listeners', listeners);
- // 保证给出的接口的一致性,使用 span 包一层,保证用户能够通过同样的方式使用 handler
- // To ensure the consistency of the given interface
- // use a span package layer to ensure that users can use the handler in the same way
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
- return () => <span {...listeners} style={{ lineHeight: 0 }} onMouseDown={(e) => {
- listeners.onMouseDown(e);
- // 阻止onMousedown的事件传递,
- // 防止元素在点击后被卸载导致tooltip/popover的弹出层意外关闭
- // Prevent the onMousedown event from being delivered,
- // preventing the element from being unloaded after being clicked,
- // causing the tooltip/popover pop-up layer to close unexpectedly
- e.preventDefault();
- e.stopPropagation();
- }}
- ><WrapperComponent /></span>;
- }, [listeners]);
- const itemCls = cls(
- `${prefix}-sortable-item`,
- {
- [`${prefix}-sortable-item-over`]: isOver,
- [`${prefix}-sortable-item-active`]: active?.id === id,
- }
- );
- const wrapperStyle = useMemo(() => {
- return (!isNull(animation)) ? {
- transform: cssDndKit.Transform.toString({
- ...transform,
- scaleX: 1,
- scaleY: 1,
- }),
- transition: transition,
- } : undefined;
- }, [animation, transform, transition]);
- return <div
- ref={setNodeRef}
- style={wrapperStyle}
- className={itemCls}
- {...attributes}
- >
- {renderItem({ id, sortableHandle }) as JSX.Element}
- </div>;
- }
|