拖拽排序实现

背景

工作中遇到一个项目,需要实现拖拽调整顺序,所以调研了一下拖动排序的实现方式。

原生实现

相关事件

HTML5 提供了 拖拽API ,要实现一个简单的拖拽功能只需要使用以下三个事件

  • dragstart:在拖动开始时做一些处理,比如记录拖动的元素,给拖动的元素添加一些样式等
  • dragover:控制元素移动过程中的元素交换表现
  • dragend:在拖动结束时清除拖动开始时设定的事件
一个简单的实现
// 根据拖动元素拖动到的位置,来重新排序列表,实现动效
const updateList = useCallback(
  (positionX, positionY) => {
    const wrapperRect = dragWrapperRef.current?.getBoundingClientRect();
    if (!wrapperRect) {
      return;
    }
    const offsetX = positionX - wrapperRect.left;
    const offsetY = positionY - wrapperRect.top;
    if (
      !dragItemRef ||
      !dragItemRef.current ||
      offsetX < 0 ||
      offsetY < 0 ||
      offsetX > wrapperRect.width ||
      offsetY > wrapperRect.height
    ) {
      return null;
    }
    const newIndex =
      Math.floor(offsetX / (ITEM_MARGIN * 2 + ITEM_WIDTH)) +
      Math.floor(offsetY / (ITEM_MARGIN * 2 + ITEM_HEIGHT)) * COLUMN;
    let copy = [...dataList];
    let dragIndex = dataList.indexOf(dragItemRef.current);
    if (dragIndex < 0) {
      return null;
    }
    copy.splice(dragIndex, 1);
    if (dragIndex < newIndex) {
      copy.splice(newIndex + 1, 0, dragItemRef.current);
    } else {
      copy.splice(newIndex, 0, dragItemRef.current);
    }
    setDataList(copy);
  },
  [dataList]
);

// 得到拖动元素的引用,在拖动时给拖动背景加上样式
const handleDragStart = (event: DragEvent<HTMLElement>, item: DragDataItem) => {
  dragItemRef.current = item;
  const el = dragWrapperRef.current?.querySelector(
    `[data_id="data_id_${item.id}"]`
  );
  if (dragWrapperRef) {
    el.classList.add(styles.dragItemActive);
  }
};
// 当元素拖动过来时更新列表,交换位置
const handleDragOver = useCallback(
  (event: DragEvent<HTMLElement>) => {
    event.preventDefault();
    updateList(event.clientX, event.clientY);
  },
  [updateList]
);

// 拖动结束后,清空拖动开始的设置
const handleDragEnd = (event: DragEvent<HTMLElement>) => {
  const item = dragItemRef.current;
  const el = dragWrapperRef.current?.querySelector(
    `[data_id="data_id_${item.id}"]`
  );
  if (el) {
    el.classList.remove(styles.dragItemActive);
  }
  dragItemRef.current = null;
};

复制代码
实现效果

原生拖拽原图.gif

缺点
  1. 封装性差,如果要做一个通用的组件的话需要自己封装
  2. 样式需要自己设置,动效什么的也需要自己写
  3. 要达到理想的效果,需要自己进行大量开发,代码量大,开发周期长

react-dnd

官方文档

安装
npm i -s react-dnd react-dnd-html5-backend
复制代码
原理

react-dnd 是一个高阶组件库,用相应的API包裹子组件,即可使其可以拖拽

  1. item 描述拖拽组件的纯JS对象
  2. type 代表 item 的类型,标志哪些拖拽源可以放在哪些目标上
  3. monitor 对象暴露组件的拖拽状态,通过其 API 可以实现当拖拽状态改变时更新组件的 props
  4. connector 对象定义事件监听的组件,给组件绑定预定义的角色例如拖拽源,拖放目标。返回的结果是记忆的,不会破坏 shouldComponentUpdate 优化
  5. collect 函数定义函数,从 monitor 中获取状态, 从 connector 中获取对象,react-dnd 定时调用 collect函数,并将其返回值合并后传递给你组件的 props,决定注入到组件的 props
  6. dragSource 可拖拽源,注册为特定的 type,根据 props 返回特定的组件
  7. dropTarget 类似 dragSource,不同之处是可以注册多个 type,可以处理多种 type 的组件的 hover 或 drop
  8. Backends 使用的拖拽事件 API,默认是 HTML5,但是有兼容问题,对触屏和 IE 不友好
  9. Hook API:useDrag,useDrop和useDragLayer
一个简单实现
const Demo: FunctionComponent = () => {
  const [componentList, setComponentList] = useState(ComponentList)
  // 排序方法
  const moveCard = useCallback((dragIndex, hoverIndex) => {
    let copy = [...componentList]
    let item = copy[dragIndex]
    copy.splice(dragIndex, 1)
    copy.splice(hoverIndex, 0, item)
    setComponentList(copy)
  }, [componentList])
  return (
    <div className={s.dndWrapper}>
      <DndProvider backend={HTML5Backend}>
      <div className={`${s.dropAreaWrapper} wrapper`}>
        {
          componentList.map((item, index)=>{
            return <ComponentListItem key={item.id} id={item.id} index={index} text={item.text} index={index} moveCard={moveCard}/>
          })
        }
      </div>
      </DndProvider>
    </div>
  )
}
export default Demo

// 基础拖拽组件
const ComponentListItem: FunctionComponent = ({id, text, index, moveCard}) => {
  const ref = useRef(null)
  const [{handlerId}, drop] = useDrop({
    accept: ComponentDropType,
    // 实现实时交换的动效
    hover(item, monitor) {
      if(!ref.current){
        return null
      }
      const dragIndex = item.index
      const hoverIndex = index
      if(dragIndex === hoverIndex){
        return
      }
      const hoverBoundingRect = ref.current?.getBoundingClientRect();
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      const clientOffset = monitor.getClientOffset();
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return;
      }
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return;
      }
      moveCard(dragIndex, hoverIndex);
      item.index = hoverIndex;
    }
  })
  const [{ isDragging }, drag] = useDrag({
    type: ComponentDropType,
    item: () => {
      return { 
        id, 
        index, 
        itemType: ComponentDropType, 
      }
    },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });
  const opacity = isDragging ? 0 : 1;
  drag(drop(ref));
  return (<div ref={ref} style={{ opacity }} data-handler-id={handlerId} className={s.componentListItem}>
    {text}
  </div>);
}
复制代码
实现效果

react-dnd拖拽图.gif

优点
  1. 可以用于不规则元素的拖拽,适用范围广
  2. 可扩展性强,可以实现复杂的拖拽需求
缺点
  1. 学习成本高,需要自己实现的东西过多,开发周期长
  2. 没有默认样式,需要自定义动画等表现

react-beautiful-dnd

官方文档

安装
npm install react-beautiful-dnd --save
复制代码
原理

是一个专用于列表的react拖放组件库,自带样式,支持鼠标和键盘事件,抽象性高,简单易用。
主要包含以下三个概念:

  1. DragDropContext:最外层包装组件,拖放背景,不支持嵌套
  2. Droppable:可放置拖动组件的区域,包装Draggable组件的外层组件
  3. Draggable:可以拖放的组件,必须放在Droppable组件里面
一个简单实现
const Demo: FC = () => {
  const [dropDataSource, setDropDataSource] = useState(DataSourceGenerator(12));

  const onDragStart = () => {};
  const onDragUpdate = (result, ...props) => {
    const { source, destination } = result;
  };
  // 只需要在拖动结束时做一些处理,拖动中间过程中元素的移动表现组件库都处理好了
  const onDragEnd = (result) => {
    const { source, destination } = result;
    if (!destination) {
      return;
    }
    const fromIndex = source.index;
    const fromDropId = source.droppableId;
    const toIndex = destination.index;
    const toDropId = destination.droppableId;
    // 判断一下拖动来源是不是目标拖动来源
    if (fromDropId == DropAreaId && toDropId == DropAreaId) {
      const copy = [...dropDataSource];
      const [from] = copy.splice(fromIndex, 1);
      copy.splice(toIndex, 0, from);
      setDropDataSource(copy);
    }
  };
  return (
    <DragDropContext
      onDragStart={onDragStart}
      onDragUpdate={onDragUpdate}
      onDragEnd={onDragEnd}
    >
      <Droppable droppableId={DropAreaId}>
        {(provided, snapshot) => {
          return (
            <div
              key="REACT_BEAUTIFUL_DND_DROP_WRAPPER"
              ref={provided.innerRef}
              className={`${s.dragComponentWrapper} ${
                snapshot.isDraggingOver ? s.dragComponentWrapperActive : ""
              }`}
              {...provided.droppableProps}
              style={{ height: 45 * dropDataSource.length + 20 }}
            >
              {dropDataSource.map((item, index) => {
                return (
                  <>
                    <Draggable
                      key={`${item.id}_drop`}
                      draggableId={`${item.id}_drop`}
                      index={index}
                    >
                      {(provided, snapshot) => {
                        return (
                          <>
                            <div
                              key={`${item.id}_drop`}
                              ref={provided.innerRef}
                              className={`${s.dragComponentItem} ${
                                snapshot.isDragging
                                  ? s.dragComponentItemActive
                                  : ""
                              }`}
                              {...provided.dragHandleProps}
                              {...provided.draggableProps}
                            >
                              <span key={1} className={s.dragItemIndex}>
                                [{index}]
                              </span>
                              <span key={2} className={s.dragItemContent}>
                                {item.title}
                              </span>
                            </div>
                          </>
                        );
                      }}
                    </Draggable>
                  </>
                );
              })}
              {/* 用于拖动到可放置区域的末尾时,自动扩展可放置区域 */}
              {provided.placeholder}
            </div>
          );
        }}
      </Droppable>
    </DragDropContext>
  );
};
复制代码
实现效果

react-beautiful-dnd拖拽图.gif

优点
  1. 开箱即用,封装性好,支持快速开发
  2. 对动画做了很好的处理,拖动过程十分顺滑
缺点
  1. 适用范围有限,只支持列表形式,不支持不规则区域的拖动

总结

使用了以上三种方式来实现拖动排序后,个人选择了react-beautiful-dnd来完成项目,因为使用场景是列表拖动,没有其他复杂的拖动场景,而且基于其强大的功能,可以加快项目进度。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享