iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 17
0
Modern Web

React + GraphQL 全端練習筆記系列 第 17

仿Trello - 客製化拖曳圖示

本系列文以製作專案為主軸,紀錄小弟學習React以及GrahQL的過程。主要是記下重點步驟以及我覺得需要記憶的部分,有覺得不明確的地方還請留言多多指教。

目前拖曳的時候,拖曳的圖案是預設的半透明樣式:

這個拖曳的樣式在React dnd裡是可以替換的,接著就來做這部分。

Preview

之前用useDrag的時候只回傳了兩個值的陣列,但其實他還有第三個回傳值: preview

                             //新的preview回傳值
const [{ isDragging }, drag, preview] = useDrag({
    //...
  });

preview跟drag類似,用於綁定拖曳圖示的dom,先前沒設定的時候就跟drag綁在同一個位置上,不過將preview跟drag分開綁定的話,可以實現拖曳把手的功能:

上面顯示的是將drag綁在綠色方塊(把手)上,preview綁在文字方塊上,就成了只有綠色方塊可以拖曳但拖曳圖示是整個文字方塊的效果。

這是官方的範例,附上連結:
React DnD example , Handles and Previews

同個範例裡還有將preview綁到DragPreviewImage這個部件上,讓拖曳圖示變成圖片的方法,這裡就不詳述,範例可以連到Code sandbox看程式碼。

不過preview頂多改變拖曳圖示擷取的節點畫面,半透明的樣式還是沒改掉。

Custom Drag Layer

想要更改拖曳的圖示,只能砍掉預設preview,自己建一個。
這個步驟比較複雜,先談談原理。

useDragLayer

React DnD提供useDragLayer這個hook,他並不直接幫你生成拖曳圖示,而單純只是個存取拖曳時的 monitor 屬性的端口。

有monitor,就能用monitor的getSourceClientOffset、getItem等方法知道目前拖曳物件的座標、item屬性等等。再來就是利用這些資訊,生成想要的拖曳圖示。

這邊就直接用官方的範例做模板,改成仿Trello要用的custom drag layer

官方範例連結

//CustomDragLayer.jsx
import React from "react";
import { ItemTypes } from "../dnd/constants.js";
import { useDragLayer } from "react-dnd";
import Todo from "./Todo";
import ListTitle from "./ListTitle";
import { Button } from "react-bootstrap";

const layerStyles = {
  position: "fixed",
  pointerEvents: "none",
  zIndex: 100,
  left: 0,
  top: 0,
};

function getItemStyles(initialOffset, currentOffset) {
  if (!initialOffset || !currentOffset) {
    return {
      display: "none",
    };
  }
  let { x, y } = currentOffset;
  const transform = `translate(${x}px, ${y}px) rotate(5deg)`;
  return {
    transform,
    WebkitTransform: transform,
  };
}

export default function CustomDragLayer() {
  const {
    item,
    itemType,
    initialOffset,
    currentOffset,
    isDragging,
  } = useDragLayer((monitor) => ({
    item: monitor.getItem(),
    itemType: monitor.getItemType(),
    initialOffset: monitor.getInitialSourceClientOffset(),
    currentOffset: monitor.getSourceClientOffset(),
    isDragging: monitor.isDragging(),
  }));
  function renderItem() {
    switch (itemType) {
      case ItemTypes.TODO: //return pure todo
        return (
          <div className="list-wrapper">
            <div className="list rounded-lg">
              <div className="todo text-wrap my-1 p-2 rounded ">
                {item.name}
              </div>
            </div>
          </div>
        );
      case ItemTypes.List: //return pure list
        return (
          <div className="list-wrapper">
            <div className="list p-2 m-1 rounded-lg ">
              <ListTitle title={item.title} />
              {item.todos.map((todo, index) => (
                <Todo
                  key={todo.id}
                  {...todo}
                  listId={item.listId}
                  todoId={index}
                />
              ))}
              <div className="footer d-flex">
                <Button className="py-1 flex-grow-1 text-left">+ New</Button>
              </div>
            </div>
          </div>
        );
      default:
        return null;
    }
  }
  if (!isDragging) {
    return null;
  }
  return (
    <div style={layerStyles} className="drag-layer">
      <div style={getItemStyles(initialOffset, currentOffset)}>
        {renderItem()}
      </div>
    </div>
  );
}

簡介各個部份的作用:

  • layerStyles: 整個拖曳圖示層的樣式,我們希望這層會覆蓋整個畫面,顯示在最上層。想改圖示的透明度也可以寫在這。
  • getItemStyles: 主要是改變拖曳圖示的座標,好讓圖示跟著游標跑,這邊多加了rotate,讓圖示轉個角度。

CustomDragLayer裡面:

  • useDragLayer: 從monitor裡提取出 item 、currentOffset等屬性。
  • renderItem: 利用剛剛從useDragLayer提出的itemTpye、item屬性,決定回傳的JSX物件。 (這裡如果將拖曳物件像是List的顯示與事件方法拆成兩部件,在這邊只取用顯示的部件的話會顯得更俐落,不過一開始我這裡就用暴力法,整個JSX複製一遍。)
  • if (!isDragging) : 沒拖曳沒圖示,就醬。

最後就是把前面的style綁到外層,然後用renderItem決定顯示的拖曳圖示。

基本上就是把拖曳圖示當成一個獨立的部件,只是利用monitor提供的屬性讓看起來是在拖曳著一樣,要怎麼改都可以。

最後要將做好的CustomDragLayer放到KanBan上:

//KanBan.jsx
export default function KanBan({ lists }) {
  return (
    <>
      <KanBanNav />
      <CustomDragLayer />   //加上CustomDragLayer
      <div className="board  p-1">
        {lists.map((list, index) => (
          <List key={list.id} {...list} listId={index} />
        ))}
        <NewList />
        <Edit />
        <ListMenu />
      </div>
    </>
  );
}

來看看效果:

恩...新的圖示跟原本的擠成一團了。

因為CustomDragLayer是另外加的部件,跟原本的Drag preview毫無關係,所以原生的圖示還好好的在工作,應該讓它消失。

可以利用useEffect在部件生成的時候用空圖片取代preview:

//Todo.jsx (List.jsx也同樣的操作)
import { getEmptyImage } from "react-dnd-html5-backend";

export default function Todo({...}){

    //...
    const [{ isDragging }, drag, preview] = useDrag({...})

    useEffect(() => {
        //將preview替換成空圖片
        preview(getEmptyImage(), { captureDraggingState: true });
    }, []);

}

讓預設圖示消失後就正常了:

React DnD的部分到這裡告一段落,不過其實DnD還有許多詳細的設定,像是useDrag有begin設定起始拖曳時執行的事件,useDrop的hover設定拖曳到拖放區上時的事件等等,都是很實用的功能,有興趣可在官方文件裡看看,說明都挺詳細,也有很多範例。

References:


上一篇
仿Trello - 用React DnD製作拖放(drop)功能
下一篇
仿Trello - 串接Unsplash API
系列文
React + GraphQL 全端練習筆記30

尚未有邦友留言

立即登入留言