本系列文以製作專案為主軸,紀錄小弟學習React以及GrahQL的過程。主要是記下重點步驟以及我覺得需要記憶的部分,有覺得不明確的地方還請留言多多指教。
目前拖曳的時候,拖曳的圖案是預設的半透明樣式:
這個拖曳的樣式在React dnd裡是可以替換的,接著就來做這部分。
之前用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頂多改變拖曳圖示擷取的節點畫面,半透明的樣式還是沒改掉。
想要更改拖曳的圖示,只能砍掉預設preview,自己建一個。
這個步驟比較複雜,先談談原理。
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>
);
}
簡介各個部份的作用:
CustomDragLayer裡面:
最後就是把前面的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設定拖曳到拖放區上時的事件等等,都是很實用的功能,有興趣可在官方文件裡看看,說明都挺詳細,也有很多範例。