Closed ndyudev closed 2 days ago
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, closestCorners, pointerWithin} from '@dnd-kit/core' import { arrayMove } from '@dnd-kit/sortable' import { useEffect, useState, useCallback } from 'react' import { cloneDeep, first } from 'lodash'
import Column from './ListColumns/Column/Column' import Card from './ListColumns/Column/ListCards/Card/Card' // import { cardActionAreaClasses } from '@mui/material'
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)) } // Function chung xử lý việc Cập nhập lại state trong trường hợp di chuyển Card giữa các Column khác nhau. const moveCardBetweenDifferentColumns = ( overColumn, OverCardId, active, over, activeColumn, activeDraggingCardId, activeDraggingCardData ) => { 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)
// phải cập nhập lại chuẩn lại dữ liệu columnId trong card sau khi kéo card giữa 2 column khác nhau.
const rebuild_activeDraggingCardData = {
...activeDraggingCardData,
column: nextOverColumn._id
}
// Tiếp theo là thêm cái card đang kéo vào overColumn theo vị trí index mới
nextOverColumn.cards = nextOverColumn.cards.toSpliced(newCardIndex, 0, rebuild_activeDraggingCardData)
nextOverColumn.cardOrderIds = nextOverColumn.cards.map(card => card._id)
}
return nextColumns
})
}
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)
// 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) {
moveCardBetweenDifferentColumns(
overColumn,
OverCardId,
active,
over,
activeColumn,
activeDraggingCardId,
activeDraggingCardData
)
}
}
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) {
moveCardBetweenDifferentColumns(
overColumn,
OverCardId,
active,
over,
activeColumn,
activeDraggingCardId,
activeDraggingCardData
)
} 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ả.
const targetColumn = nextColumns.find(column => column._id === overColumn._id)
// Cập nhập lại 2 giá trị mới là card và cardOrderIds trong cái targetColumn
targetColumn.cards = dndOrderedCards
targetColumn.cardOrderIds = dndOrderedCards.map(card => card._id)
// Trả về vị trí state mới ( chuẩn vị trí )
return nextColumns
})
}
}
// 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)
}
const customDropAnimation = { sideEffects: defaultDropAnimationSideEffects({ styles: { active: { opacity: '0.5' } } }) }
// Sẽ custom lại chiến lược / thuật tóan phát hiện va chạm tối ưu cho việc kéo thả card giữa nhiều columns // args = arguments = Các đổi số và tham số const collisionDetectionStrategy = useCallback((args) => { console.log('collisionDetectionStrategy') // So sánh thay vì gán if (activeDragItemType === ACTIVE_DRAG_ITEM_TYPE.COLUMN) { const pointerIntersections = pointerWithin(args) const intersections = !!pointerIntersections?.length ? pointerIntersections : closestCorners(args) return intersections } else { return closestCorners(args) // fallback trong trường hợp không phải cột } }, [activeDragItemType])
return ( <DndContext // Thuật tóa phát hiện va chạm ( nếu không có nó thì card với cover lớn sẽ không kéo qua Column đựoc vì lúc này nó đang bị conflict giữa card và column) // Chung ta sẽ dùng closetCorners thay vì closestCenter // collisionDetection={closestCorners} // Update video 37: nếu chỉ dùng closetsCorners sẽ có bug flickering + sai lệch dữ liệu // Tự custom nâng cao thuật toán phát hiện va chạm
collisionDetection={collisionDetectionStrategy}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
sensors={sensors}
>
<Box
sx={{
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#34495e' : '#1976d2',
width: '100%',
height: (theme) => theme.trello.BoardContentHeight,
p: '10px 0',
}}
>
<ListColumns columns={orderedColumns} />
<DragOverlay dropAnimation={customDropAnimation}>
{!activeDragItemType && null }
{activeDragItemType === ACTIVE_DRAG_ITEM_TYPE.COLUMN && <Column column={activeDragItemData} /> }
{activeDragItemType === ACTIVE_DRAG_ITEM_TYPE.CARD && <Card card={activeDragItemData} /> }
</DragOverlay>
</Box>
</DndContext>
) }
export default BoardContent