使用dnd-kit实现单选复选框列表拖拽排序checkbox(详细)

2,495 阅读4分钟

@dnd-kit 一个轻量级、模块化、高性能、可访问和可扩展的 React 拖放工具包。

原本使用的是react-sortable-hoc,在查阅该库相关文档发现作者建议采用dnd-kit替换。

因此最终决定直接使用dnd-kit库来实现拖拽排序功能。

屏幕录制-2023-08-11-145109.gif

下面给出实现代码和需要注意的点:

  1. yarn add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities @dnd-kit/modifiers

  2. 然后需要了解两个核心的组件DndContext和SortableContext

    2.1 DndContext:可以理解为dnd的容器,想要使用拖拽这是必须的,并且相应的回调监听和配置都需要附加在这里

    2.2 SortableContext:滑动列表的容器,可以类似理解为一个ul,内部的列表项配置拖拽

    这里必须注意:(items为必填项,表示哪些列表在我这个容器中滑动,items为唯一标识的数组,这个标志数组必须和后面useSortable传入的id属性一一对应)

    <SortableContext 
        items={dataList.map((d) => d.key)} 
        strategy={verticalListSortingStrategy} >
        {......}
    </SortableContext>
    
  3. 所以基本结构就是DndContext包括SortableContext再包括需要拖动的列表项

  4. <DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
    {/* 在这里配置拖动结束后的回调函数,modifiers可以限定元素可以拖动的区域 
    	这里强烈建议配置这个限定区域,不然当元素拖出了对应区域,
    	而有功能逻辑涉及时会取到null,产生错误 */}
    {/* 拖拽结束后要实现的就是移动元素的位置,同时同步给状态再传给父组件modal
    	dnd库里自带了一个arrayMove方法,但是需要传入开始和结束index
    	因为事件的回调参数是active和over对象,继续调用编写的getMoveIndex方法获得*/}
    
  5. 接下来的核心的就是如何实现我们的sortableItem

    const { setNodeRef, attributes, listeners, transform, transition } = useSortable({
                id: checkboxItem.key, 
        		// 这里传入的id属性必须和SortableContext的items数组一一对应
                transition: {
                    duration: 500,
                    easing: 'cubic-bezier(0.25, 1, 0.5, 1)',
                },
            });
    {/* 这是核心的钩子,返回值如下:
    	setNodeRef:设置ref={setNodeRef},使Dom成为一个可拖拽的项
    	listeners:包含一系列触发拖拽方法,默认是鼠标触发,附加这个属性可以灵活地设置拖拽部		位,结合配置拖拽手柄
    	transform:该节点被拖动时候的移动变化值
    	transition:过渡效果
    	isDragging:节点是否在拖拽 */}
    所以,我们在li元素挂载ref,attributes(官方推荐一起传入,防止预期之外的错误),style,
    在li元素的子元素中自定义组件内容,为拖拽手柄设置listeners即可
    
  6. 最后需要注意的一点,一般我们都会把这个排序作为一个组件的子组件,因为我们采用的受控的方式,状态由自己维护,所以需要在初始化时将状态传给父组件,否则如果既不拖动也不点击,父组件modal则会可能拿到undefined

  7. 最后,我们需要注意,因为拖动事件和点击事件存在叠加,如果事件存在交叉

    所以可以有这样的解决思路:精确化设置listeners,这样拖动开关只会生效在设置的dom元素,不然默认是在每个列表项的父元素上的

    第二,如果确有特殊需求,可以阻止默认事件冒泡,手动调度事件触发顺序

/* eslint-disable jsx-a11y/label-has-associated-control */
import { DndContext } from '@dnd-kit/core';
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { restrictToParentElement } from '@dnd-kit/modifiers';
import React, { useState, useEffect } from 'react';

// index获取算法,事件的回调包含active和over对象,无法直接用indexOf获取在原数组中的index
// 用这个方法遍历获取在原数组对应的位置index,供arrayMove方法使用
const getMoveIndex = (array, dragItem) => {
    const { active, over } = dragItem;
    let activeIndex = 0;
    let overIndex = 0;
    try {
        // 遍历数组,查找出active和over的index
        array.forEach((item, index) => {
            if (active.id === item.key) {
                activeIndex = index;
            }
            if (over.id === item.key) {
                overIndex = index;
            }
        });
    } catch (error) {
        overIndex = activeIndex; // 如果有问题,则复位
    }
    return { activeIndex, overIndex };
};

export default function DndSortComponent() {
    const [dataList, setDataList] = useState([
        { key: 'name', width: 0.25, isChecked: true, title: '姓名' },
        { key: 'age', width: 0.25, isChecked: true, title: '年龄' },
        { key: 'sex', width: 0.25, isChecked: true, title: '性别' },
        { key: 'phone', width: 0.25, isChecked: true, title: '手机号' },
    ]);
    
    useEffect(() => {
        props.onChange(dataList);// 这里写你自己对父组件传值的方式
    }, []);
    
    // 拖拽结束后的操作
    const dragEndEvent = (dragItem) => {
        setDataList((prevDataList) => {
            const moveDataList = [...prevDataList];
            const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem);
            const newDataList = arrayMove(moveDataList, activeIndex, overIndex);
            return newDataList;
        });
    };

    // 点击checkbox的操作,修改指定ID的对象的isChecked属性取反即可
    const handleCheckedChange = (chosedItemKey) => {
        setDataList((prevDataList) => {
            const newDataList = [...prevDataList];
            const updatedDataList = newDataList.map((item) => {
                if (item.key === chosedItemKey) {
                    return { ...item, isChecked: !item.isChecked };
                }
                return item;
            });
            return updatedDataList;
        });
    };

    // 拖拽项组件
    const SortableItem = (itemProps) => {
        // 父传子,从props里拿,建议使用其他名字(如itemProps)代替props,以免和父组件的props混淆
        const { checkboxItem } = itemProps;
        const { setNodeRef, attributes, listeners, transform, transition } = useSortable({
            id: checkboxItem.key, // 这里传入的id属性必须和SortableContext的items数组一一对应
            transition: {
                duration: 500,
                easing: 'cubic-bezier(0.25, 1, 0.5, 1)',
            },
        });
        const styles = {
            transform: CSS.Transform.toString(transform),
            transition,
        };
        return (
            <li ref={setNodeRef} {...attributes} style={styles}>
                <label className="check-label">
                    <input
                        type="checkbox"
                        name={checkboxItem.key}
                        checked={checkboxItem.isChecked}
                        onChange={() => handleCheckedChange(checkboxItem.key)}
                    />
                    <label className="name-label">{checkboxItem.ptitle || checkboxItem.title}</label>
                    {/* 为Dom添加listeners属性表示设置其为可拖动项 */}
                    <span className="btn all-scroll" {...listeners}>
                        &#9836;
                    </span>
                </label>
            </li>
        );
    };

    return (
        <DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
            <SortableContext items={dataList.map((c) => c.key)} strategy={verticalListSortingStrategy}>
                {/* 这里的items接收一个数组,这个数组的值要和useSortable传入的id属性一一对应 */}
                <div className="attrs">
                    <ul className="attrs-list">
                        {dataList.map((checkboxItem) => (
                            <SortableItem checkboxItem={checkboxItem} key={checkboxItem.key} />
                        ))}
                    </ul>
                </div>
            </SortableContext>
        </DndContext>
    );
}