React中使用Drag&Drop API实现列表拖动排序功能

258 阅读1分钟

最近有个需求是拖动一个列表中的item, 使列表能够调整显示的顺序,调研了一下,成熟的的工具有React-dnd, 不过感觉没必要, 因为项目中其他地方不怎么使用拖拽。遂自己使用Drag&Drop API(理论上换成mouse/touch也能实现)实现了一个简单的demo。

组件

思路: 首先,根据数据->视图的思想, 在完成拖拽后,调整数组顺序,然后再重新渲染视图。

  • Draggrable
    • DragItem
      • 用于拖拽
    • DropLine
      • 用于放置
  1. Draggrable
import React, { ReactNode, useState } from "react";
import DropLine from "./DropLine";
import DragItem from "./DragItem";
export interface BaseItem {
  id: string;
}

interface DraggableProps<T extends BaseItem> {
  data: Array<T>;
  renderItem: (item: T) => ReactNode;
}

export default function Draggable<T extends BaseItem>({
  data,
  renderItem,
}: DraggableProps<T>) {
  const [items, setItems] = useState(data);
  const [selectedID, setSelectedID] = useState("");

  const moveBefore = (anchorID: string, selectedID: string) => {
    const selectedItem = items.find((item) => item.id === selectedID);
    if (!selectedItem) return;
    let newItems = items.filter((item) => item.id !== selectedID);

    const anchorIndex = newItems.findIndex((item) => item.id === anchorID);

    newItems = [
      ...newItems.slice(0, anchorIndex + 1),
      { ...selectedItem },
      ...newItems.slice(anchorIndex + 1),
    ];
    setItems(newItems);
  };

  return (
    <section>
      {items.reduce(
        (acc, item, idx) => [
          ...acc,
          <DragItem key={item.id} {...{ selectedID, setSelectedID, item }}>
            {renderItem(item)}
          </DragItem>,
          <DropLine
            key={idx + 1}
            {...{
              anchorID: item.id,
              selectedID,
              onMove: moveBefore,
            }}
          />,
        ],
        [
          <DropLine
            key={0}
            {...{
              anchorID: "",
              selectedID,
              onMove: moveBefore,
            }}
          />,
        ]
      )}
    </section>
  );
}

  1. DropLine
  • 每个DropLine绑定一个DragItem ID,表示它的前驱(第一条DropLine没有前驱)
  • 当item进入的时候,在DragEnter设置active, 在DragLeave, Drop取消active
  • 放置后,在Drop里面移动数组
import React, { useState } from "react";
interface DropLineProps {
  selectedID: string;
  anchorID: string;
  onMove: (anchorID: string, selectedID: string) => void;
}
export default function DropLine({
  selectedID,
  anchorID,
  onMove,
}: DropLineProps) {
  const [active, setActive] = useState(false);

  return (
    <div
      style={{
        height: "2rem",
        display: "flex",
        alignItems: "center",
      }}
      onDragEnter={(e) => {
        e.preventDefault();
        setActive(true);
      }}
      onDragOver={(e) => e.preventDefault()}
      onDrop={() => {
        setActive(false);
        if (selectedID === anchorID) return;
        onMove(anchorID, selectedID);
      }}
      onDragLeave={() => {
        setActive(false);
      }}
    >
      <hr
        style={{
          height: "3px",
          flex: "1",
          opacity: `${active && selectedID !== anchorID ? 1 : 0}`,
          backgroundColor: "green",
        }}
      />
    </div>
  );
}

  1. DropItem
  • 拖拽开始的时候, 在DragStart事件设置SelectedID, 拖拽结束后取消设置
import React, { ReactNode } from "react";
import type { BaseItem } from "./Draggrable";

interface DragItemProps<T extends BaseItem> {
item: T;
selectedID: string;
setSelectedID: (id: string) => void;
children: ReactNode;
}

export default function DragItem<T extends BaseItem>({
selectedID,
setSelectedID,
item,
children,
}: DragItemProps<T>) {
return (
  <div
    key={item.id}
    draggable
    onDragStart={() => {
      setSelectedID(item.id);
    }}
    onDragEnd={() => {
      setSelectedID("");
    }}
    style={{
      height: "2rem",
      color: `${selectedID === item.id ? "red" : "black"}`,
    }}
  >
    {children}
  </div>
);
}

Animation.gif

总结

功能是实现了, 但是感觉还是很粗糙, 拖动过程的反馈还是太差了。