function BoardContent({ board }) {
// Nếu dùng PointerSensor mặc định phải kết hợp thuộc tính CSS touch-action: none ở các phần tử kéo thả - Nhưng còn bug
// const pointerSensor = useSensor(PointerSensor, { activationConstraint:{ distance: 10}})
// Yêu cầu chuột di chuyển 10px thì mới kích hoạt event, fix trường hợp click bị gọi event
const mouseSensor = useSensor(MouseSensor, { activationConstraint:{ distance: 10}})
// Nhấn giữ 250ms và dung sai của cảm ứng (di chuyển chênh lệch 500px) thì mới kích hoạt event
const toucherSensor = useSensor(TouchSensor, { activationConstraint:{ delay: 250, tolerance: 500}})
// Ưu tiên sử dụng kết hợp 2 loại sensors là mouse và touch để có trải nghiệm trên mobile tốt nhất không bị bug
// const sensors = useSensors(pointerSensor)
const sensors = useSensors(mouseSensor, toucherSensor)
// Cùng một thời điểm chỉ có một phần tử được kéo (column hoặc card)
const [activeDragItemId, setActiveDragItemID] = useState(null)
const [activeDragItemType, setActiveDragItemType] = useState(null)
const [activeDragItemData, setActiveDragItemData] = useState(null)
const [oldColumnWhenDraggingCard, setOldColumnWhenDraggingCard] = useState(null)
// Tìm một column theo cardId
const findColumnByCardId = (cardId) => {
// Dùng c.cards thay vì c.cardOrderIds vì trong handleDragOver dữ liệu cards sẽ hoàn chỉnh trước khi tạo CardOrderIds mới
return orderedColumns.find(column => column?.cards?.map(card => card._id)?.includes(cardId))
}
// Nếu là kéo card thì mới thực hiện hành động như set giá trị oldColumn
if(event?.active?.data?.current?.columnId) {
setOldColumnWhenDraggingCard(findColumnByCardId(event?.active?.id))
}
}
// Trigger trong quá trình kéo
const handleDragOver = (event) => {
// Không làm gì nếu đang kéo column
if (activeDragItemType === ACTIVE_DRAG_ITEM_TYPE.COLUMN) return
// console.log('handleDragOver:', event)
const { active, over } = event
if (!active || !over) return
const { id: activeDraggingCardId, data: { current: activeDraggingCardData }} = active
const { id: OverCardId } = over
const activeColumn = findColumnByCardId(activeDraggingCardId)
const overColumn = findColumnByCardId(OverCardId)
// Nếu không tồn tại 1 trong 2 column thì không làm gì để tránh crash trang web
if (!activeColumn || !overColumn) return
// Xử lý logic khi kéo card giữa các columns khác nhau
if (activeColumn._id !== overColumn._id) {
setOrderedColumns(prevColumns => {
const overCardIndex = overColumn?.cards?.findIndex(card => card._id === OverCardId)
let newCardIndex
const isBelowOverItem = active.rect.current.translated && active.rect.current.translated.top > over.rect.top + over.rect.height
const modifier = isBelowOverItem ? 1 : 0
newCardIndex = overCardIndex >= 0 ? overCardIndex + modifier : overColumn?.cards?.length + 1
// Clone mảng OrderredColumnsState cũ ra một cái mới để xử lý data rồi return - cập nhập lại OrderedColumnsState mới
const nextColumns = cloneDeep(prevColumns)
const nextActiveColumn = nextColumns.find(column => column._id === activeColumn._id)
const nextOverColumn = nextColumns.find(column => column._id === overColumn._id)
if (nextActiveColumn) {
nextActiveColumn.cards = nextActiveColumn.cards.filter(card => card._id !== activeDraggingCardId)
nextActiveColumn.cardOrderIds = nextActiveColumn.cards.map(card => card._id)
}
if (nextOverColumn) {
nextOverColumn.cards = nextOverColumn.cards.filter(card => card._id !== activeDraggingCardId)
nextOverColumn.cards = nextOverColumn.cards.toSpliced(newCardIndex, 0, activeDraggingCardData)
nextOverColumn.cardOrderIds = nextOverColumn.cards.map(card => card._id)
}
return nextColumns
})
}
}
const handleDragEnd = (event) => {
const { active, over } = event
if (!active || !over) return
if (activeDragItemType === ACTIVE_DRAG_ITEM_TYPE.CARD) {
const { id: activeDraggingCardId, data: { current: activeDraggingCardData }} = active
const { id: OverCardId } = over
const activeColumn = findColumnByCardId(activeDraggingCardId)
const overColumn = findColumnByCardId(OverCardId)
// Nếu không tồn tại 1 trong 2 column thì không làm gì để tránh crash trang web
if (!activeColumn || !overColumn) return
//Keos card qua 2 column khasc nhau
// Phải dung tới activeDragItemData ( set vào state từ bước handleDragStart ) chứ không phải acticeData
// tong scope handleDragEnd này vì sau khi đi qua onDragOver tới đây là stats của card đã bị cập nhập một lần rồi.
if (oldColumnWhenDraggingCard._id !== overColumn._id) {
//
} else {
// hành động kéo thả card trong cùng 1 column
// Lấy vị trí cũ ( từ thằng oldColumnWhenDraggingCard)
const oldCardIndex = oldColumnWhenDraggingCard?.cards?.findIndex(c => c._id === activeDragItemId)
// Lấy vị trí mới ( từ thằng )
const newCardIndex = overColumn?.cards?.findIndex(c => c._id === OverCardId)
// Dùng arrayMove vì kéo card trong một cái column thì tương tự với logic
const dndOrderedCards = arrayMove(oldColumnWhenDraggingCard?.cards, oldCardIndex, newCardIndex)
setOrderedColumns(prevColumns => {
// Clone mảng OrderredColumnsState cũ ra một cái mới để xử lý data rồi return - cập nhập lại OrderedColumnsState mới
const nextColumns = cloneDeep(prevColumns)
// Tìm tới column mà chúng ta đang thả.
})
}
}
// Xử lý kleos thả Columns trong cùng 1 một cái boardContent
if (activeDragItemType === ACTIVE_DRAG_ITEM_TYPE.COLUMN) {
if (active.id !== over.id) {
// Lấy vị trí cũ ( từ thằng active )
const oldColumnIndex = orderedColumns.findIndex(c => c._id === active.id)
// Lấy vị trí mới ( từ thằng over )
const newColumnIndex = orderedColumns.findIndex(c => c._id === over.id)
// Dùng arrayMove của thằng dnd-kit để sắp xếp lại mảng Columns ban đầu
// Code của arrayMove ở đây : Dnd - kit / ArrayMove.ts
const dndOrderedColumns = arrayMove(orderedColumns, oldColumnIndex, newColumnIndex)
setOrderedColumns(dndOrderedColumns)
}
}
// Những dữ liệu sau khi kéo thả này luôn phải đưa về giá trị null mặc định ban đầu
setActiveDragItemID(null)
setActiveDragItemData(null)
setActiveDragItemType(null)
setOldColumnWhenDraggingCard(null)
import Box from '@mui/material/Box' import ListColumns from './ListColumns/ListColumns' import { mapOrder } from '~/utils/sorts' import { DndContext, // PointerSensor, useSensor, useSensors, MouseSensor, TouchSensor, DragOverlay, defaultDropAnimationSideEffects } from '@dnd-kit/core' import { arrayMove } from '@dnd-kit/sortable' import { useEffect, useState } from 'react' import { cloneDeep } from 'lodash'
import Column from './ListColumns/Column/Column' import Card from './ListColumns/Column/ListCards/Card/Card'
const ACTIVE_DRAG_ITEM_TYPE = { COLUMN: 'ACTIVE_DRAG_ITEM_TYPE_COLUMN', CARD: 'ACTIVE_DRAG_ITEM_TYPE_CARD' }
function BoardContent({ board }) { // Nếu dùng PointerSensor mặc định phải kết hợp thuộc tính CSS touch-action: none ở các phần tử kéo thả - Nhưng còn bug // const pointerSensor = useSensor(PointerSensor, { activationConstraint:{ distance: 10}}) // Yêu cầu chuột di chuyển 10px thì mới kích hoạt event, fix trường hợp click bị gọi event const mouseSensor = useSensor(MouseSensor, { activationConstraint:{ distance: 10}}) // Nhấn giữ 250ms và dung sai của cảm ứng (di chuyển chênh lệch 500px) thì mới kích hoạt event const toucherSensor = useSensor(TouchSensor, { activationConstraint:{ delay: 250, tolerance: 500}}) // Ưu tiên sử dụng kết hợp 2 loại sensors là mouse và touch để có trải nghiệm trên mobile tốt nhất không bị bug // const sensors = useSensors(pointerSensor) const sensors = useSensors(mouseSensor, toucherSensor)
const [orderedColumns, setOrderedColumns] = useState([])
// Cùng một thời điểm chỉ có một phần tử được kéo (column hoặc card) const [activeDragItemId, setActiveDragItemID] = useState(null) const [activeDragItemType, setActiveDragItemType] = useState(null) const [activeDragItemData, setActiveDragItemData] = useState(null) const [oldColumnWhenDraggingCard, setOldColumnWhenDraggingCard] = useState(null)
useEffect(() => { setOrderedColumns(mapOrder(board?.columns, board?.columnOrderIds, '_id')) }, [board])
// Tìm một column theo cardId const findColumnByCardId = (cardId) => { // Dùng c.cards thay vì c.cardOrderIds vì trong handleDragOver dữ liệu cards sẽ hoàn chỉnh trước khi tạo CardOrderIds mới return orderedColumns.find(column => column?.cards?.map(card => card._id)?.includes(cardId)) }
const handleDragStart = (event) => { setActiveDragItemID(event?.active?.id) setActiveDragItemType(event?.active?.data?.current?.columnId ? ACTIVE_DRAG_ITEM_TYPE.CARD : ACTIVE_DRAG_ITEM_TYPE.COLUMN) setActiveDragItemData(event?.active?.data?.current) // console.log('handleDragStart:', event)
}
// Trigger trong quá trình kéo const handleDragOver = (event) => { // Không làm gì nếu đang kéo column if (activeDragItemType === ACTIVE_DRAG_ITEM_TYPE.COLUMN) return // console.log('handleDragOver:', event)
}
const handleDragEnd = (event) => { const { active, over } = event if (!active || !over) return
}
const customDropAnimation = { sideEffects: defaultDropAnimationSideEffects({ styles: { active: { opacity: '0.5' } } }) }
return ( <DndContext onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd} sensors={sensors}
export default BoardContent