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 最佳實踐

相關文章
相關標籤/搜索