React 最佳实践:亲手码一个「可拖放列表」

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战css

前言

拖放 在实际业务中也是一个常见的功能,好比经过拖放列表元素进行排序、拖放元素在不一样的容器中穿梭等。提到拖放,咱们想到的是原生 DOM 事件的操做、元素移动时位置的计算以及动画等等,第一反应必然是找第三方拖放库 ‘帮助’ 咱们完成需求咯。然而,第三方库虽然功能强大,但不少功能你未必须要,杀鸡焉用牛刀呢?react

因此咱们能够根据实际场景的复杂度来选择实现方式,第三方库的强大功能 or 本身实现最精炼的代码git

本身动手(代码不过百)

实现一个列表拖放排序的功能,选中元素后,元素实时跟随鼠标移动,而且列表实时更新,以下图: dragdrop.gifgithub

准备工做

在 React 中实现拖放的技术要点:npm

  • 熟悉鼠标事件:MouseDownMouseUpMouseMove
  • 如何触发拖放开始(MouseDown)和判断拖放结束(MouseUp
  • 如何实现拖放元素位置的移动,能够用两种方案
    • 原元素跟着鼠标移动
    • 建立一个新元素,做为原元素的影子,跟随鼠标移动
  • 在组件中维护拖放的状态

拖放开始

在行元素上监听 Mousedown 事件,在 Mousedown 中初始化拖放状态:markdown

  • dragging:true,拖放中
  • startPageY,记录鼠标开始位置(纵向)
  • draggingIndex,选中元素的 index
<li onMouseDown={(evt) => handleMounseDown(evt, i)} style={getDraggingStyle(i)}>
  {text}
</li>
复制代码
const handleMounseDown = (evt, index) => {
  setDragging(true);
  setStartPageY(evt.pageY);
  setDraggingIndex(index);
};
复制代码

样式上高亮拖放元素而且添加跟随鼠标效果,translateX 能够产生视觉上悬浮的效果,其中的 offsetPageY 的值将会在拖放过程当中(MouseMove)计算得来:antd

const getDraggingStyle = (index) => {
  if (index !== draggingIndex) return {};
  return {
    backgroundColor: 'lightsteelblue',
    transform: `translate(10px, ${offsetPageY}px)`,
  };
};
复制代码

拖动中

拖放中最关键的地方是:该在哪一个节点上监听 MouseMove 事件?ide

用户拖动时,鼠标是能够全页面跑的,若是在拖动元素( <li/> )或者父元素( <ul/> )上监听事件都不合适。此时有一个方案:建立一个透明的全局的 fixed mask,既覆盖了鼠标的运动轨迹,又防止触发其余元素。oop

{
  dragging && (
    <div className="my-dnd-mask" onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} />
  );
}
复制代码
.my-dnd-mask {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0);
}
复制代码

在 MouseMove 中,就是核心计算代码:post

  • 计算偏移值(offset),let offset = evt.pageY - startPageY;
  • 当偏移值大于行高时,进行位置交换(move),并从新设置 DraggingIndexStartPageY
  • 当偏移值小于行高时,操做同上
  • 当偏移值没有超过行高,只须要设置 offsetPageY
const move = (arr, startIndex, toIndex) => {
  arr = arr.slice();
  arr.splice(toIndex, 0, arr.splice(startIndex, 1)[0]);
  return arr;
};
const handleMouseMove = (evt) => {
  let offset = evt.pageY - startPageY;
  if (offset > lineHeight && draggingIndex < list.length - 1) {
    // move down
    offset -= lineHeight;
    setList((pre) => move(pre, draggingIndex, draggingIndex + 1));
    setDraggingIndex((pre) => pre + 1);
    setStartPageY((pre) => pre + lineHeight);
  } else if (offset < -lineHeight && draggingIndex > 0) {
    // move up
    offset += lineHeight;
    setList((pre) => move(pre, draggingIndex, draggingIndex - 1));
    setDraggingIndex((pre) => pre - 1);
    setStartPageY((pre) => pre - lineHeight);
  }
  setOffsetPageY(offset);
};
复制代码

拖放结束

设置 dragging: false,mask 层消失, startPageYdraggingIndex 初始化。

const handleMouseUp = () => {
  setDragging(false);
  setStartPageY(0);
  setDraggingIndex(-1);
};
复制代码

完整代码

完整代码能够看 这里

拖放库

02.gif

找了一个在 React 最有名的拖放库,react-beautiful-dnd

安装

# yarn
yarn add react-beautiful-dnd

# npm
npm install react-beautiful-dnd --save
复制代码

使用 react-beautiful-dnd

import { DragDropContext } from 'react-beautiful-dnd';
复制代码

基础使用

须要咱们管理的部分:

  • 展现的数据
  • 拖放结束后的动做(列表重排序)
  • 拖放时的变化(样式)

其他的事情,react-beautiful-dnd 会办好的。

Dom 结构

<DragDropContext onDragEnd={onDragEnd}>
  <center> <Droppable droppableId="droppable"> {(provided, snapshot) => { return ( <div ref={provided.innerRef}> {items.map((item, index) => ( <Draggable key={item.id} draggableId={item.id} index={index}> {(provided, snapshot) => ( <div ref={provided.innerRef}>{item.content}</div> )} </Draggable> ))} {provided.placeholder} </div> ); }} </Droppable> </center>
</DragDropContext>
复制代码

拖放结束,从新排序

// 元素移动
const move = (arr, startIndex, toIndex) => {
  arr = arr.slice();
  arr.splice(toIndex, 0, arr.splice(startIndex, 1)[0]);
  return arr;
};

const onDragEnd = (result) => {
  if (!result.destination) {
    return;
  }
  setItems((pre) => move(pre, result.source.index, result.destination.index));
};
复制代码

拖放中,样式管理

// 设置样式
const getItemStyle = (isDragging, draggableStyle) => ({
  userSelect: 'none',
  padding: grid * 2,
  margin: `0 0 ${grid}px 0`,
  // 拖拽的时候,item 背景变化
  background: isDragging ? 'lightgreen' : '#ffffff',
  ...draggableStyle,
});

const getListStyle = (isDraggingOver) => {
  return {
    // 拖拽的时候,list 背景变化
    background: isDraggingOver ? 'darkgreen' : 'gray',
    padding: 8,
    width: 250,
  };
};

<Droppable droppableId="droppable"> {(provided, snapshot) => { return ( <div style={getListStyle(snapshot.isDraggingOver)}> {items.map((item, index) => ( <Draggable> {(provided, snapshot) => ( <div style={getItemStyle( snapshot.isDragging, provided.draggableProps.style, )} ></div> )} </Draggable> ))} </div> ); }} </Droppable>;
复制代码

效果预览

02.gif

完整代码

完整代码能够看 这里

进阶操做

详细的 API 介绍和进阶操做(容器间的拖拽 & 容器可滚动),看这边

React 最佳实践

相关文章
相关标签/搜索