228 lines
6.1 KiB
TypeScript
228 lines
6.1 KiB
TypeScript
|
|
import React from 'react'
|
|||
|
|
import {
|
|||
|
|
DndContext,
|
|||
|
|
DragOverlay,
|
|||
|
|
closestCenter,
|
|||
|
|
KeyboardSensor,
|
|||
|
|
PointerSensor,
|
|||
|
|
useSensor,
|
|||
|
|
useSensors,
|
|||
|
|
DragStartEvent,
|
|||
|
|
DragEndEvent,
|
|||
|
|
} from '@dnd-kit/core'
|
|||
|
|
import {
|
|||
|
|
SortableContext,
|
|||
|
|
sortableKeyboardCoordinates,
|
|||
|
|
verticalListSortingStrategy,
|
|||
|
|
} from '@dnd-kit/sortable'
|
|||
|
|
import { MenuItemComponent } from './MenuItemComponent'
|
|||
|
|
import { MenuItem } from '@/@types/menu'
|
|||
|
|
|
|||
|
|
interface SortableMenuTreeProps {
|
|||
|
|
items: MenuItem[]
|
|||
|
|
onItemsChange: (items: MenuItem[]) => void
|
|||
|
|
isDesignMode: boolean
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
|||
|
|
items,
|
|||
|
|
onItemsChange,
|
|||
|
|
isDesignMode,
|
|||
|
|
}) => {
|
|||
|
|
const [activeItem, setActiveItem] = React.useState<MenuItem | null>(null)
|
|||
|
|
|
|||
|
|
const sensors = useSensors(
|
|||
|
|
useSensor(PointerSensor, {
|
|||
|
|
activationConstraint: {
|
|||
|
|
distance: 8,
|
|||
|
|
},
|
|||
|
|
}),
|
|||
|
|
useSensor(KeyboardSensor, {
|
|||
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|||
|
|
}),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Flatten the tree structure to get all items with their paths
|
|||
|
|
const flattenItems = (
|
|||
|
|
items: MenuItem[],
|
|||
|
|
parentPath: string[] = [],
|
|||
|
|
): Array<{ item: MenuItem; path: number[] }> => {
|
|||
|
|
const result: Array<{ item: MenuItem; path: number[] }> = []
|
|||
|
|
|
|||
|
|
items.forEach((item, index) => {
|
|||
|
|
const currentPath = [...parentPath, index]
|
|||
|
|
result.push({ item, path: currentPath })
|
|||
|
|
|
|||
|
|
if (item.children && item.children.length > 0) {
|
|||
|
|
result.push(...flattenItems(item.children, currentPath))
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Find item by ID in the tree
|
|||
|
|
const findItemById = (items: MenuItem[], id: string): MenuItem | null => {
|
|||
|
|
for (const item of items) {
|
|||
|
|
if (item.id === id) {
|
|||
|
|
return item
|
|||
|
|
}
|
|||
|
|
if (item.children) {
|
|||
|
|
const found = findItemById(item.children, id)
|
|||
|
|
if (found) return found
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Remove item from tree by ID
|
|||
|
|
const removeItemFromTree = (items: MenuItem[], id: string): MenuItem[] => {
|
|||
|
|
return items.reduce((acc: MenuItem[], item) => {
|
|||
|
|
if (item.id === id) {
|
|||
|
|
return acc // Skip this item (remove it)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const newItem = { ...item }
|
|||
|
|
if (newItem.children && newItem.children.length > 0) {
|
|||
|
|
newItem.children = removeItemFromTree(newItem.children, id)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
acc.push(newItem)
|
|||
|
|
return acc
|
|||
|
|
}, [])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Insert item at specific position
|
|||
|
|
const insertItemAtPath = (
|
|||
|
|
items: MenuItem[],
|
|||
|
|
item: MenuItem,
|
|||
|
|
targetPath: number[],
|
|||
|
|
): MenuItem[] => {
|
|||
|
|
if (targetPath.length === 1) {
|
|||
|
|
const newItems = [...items]
|
|||
|
|
newItems.splice(targetPath[0], 0, item)
|
|||
|
|
return newItems
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const [firstIndex, ...restPath] = targetPath
|
|||
|
|
const newItems = [...items]
|
|||
|
|
|
|||
|
|
if (newItems[firstIndex]) {
|
|||
|
|
newItems[firstIndex] = {
|
|||
|
|
...newItems[firstIndex],
|
|||
|
|
children: insertItemAtPath(newItems[firstIndex].children || [], item, restPath),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return newItems
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get the path where an item should be inserted based on over item
|
|||
|
|
const getInsertionPath = (
|
|||
|
|
items: MenuItem[],
|
|||
|
|
activeId: string,
|
|||
|
|
overId: string,
|
|||
|
|
): number[] | null => {
|
|||
|
|
const flatItems = flattenItems(items)
|
|||
|
|
const activeIndex = flatItems.findIndex(({ item }) => item.id === activeId)
|
|||
|
|
const overIndex = flatItems.findIndex(({ item }) => item.id === overId)
|
|||
|
|
|
|||
|
|
if (overIndex === -1) return null
|
|||
|
|
|
|||
|
|
const overItem = flatItems[overIndex]
|
|||
|
|
const insertPath = [...overItem.path]
|
|||
|
|
|
|||
|
|
// Aktif item, listedeki over item'den sonra geliyorsa, yukarı taşınıyordur → over item'ın yerine ekle
|
|||
|
|
// Aktif item, listedeki over item'den önceyse, aşağı taşınıyordur → bir SONRASINA ekle ki yeniden aynı yere düşmesin
|
|||
|
|
if (activeIndex < overIndex) {
|
|||
|
|
insertPath[insertPath.length - 1] += 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return insertPath
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateOrderNumbers = (items: MenuItem[]): MenuItem[] => {
|
|||
|
|
return items.map((item, index) => ({
|
|||
|
|
...item,
|
|||
|
|
order: index + 1,
|
|||
|
|
children: item.children ? updateOrderNumbers(item.children) : [],
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleDragStart = (event: DragStartEvent) => {
|
|||
|
|
const { active } = event
|
|||
|
|
const activeItem = findItemById(items, active.id as string)
|
|||
|
|
setActiveItem(activeItem)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|||
|
|
const { active, over } = event
|
|||
|
|
setActiveItem(null)
|
|||
|
|
|
|||
|
|
if (!over || active.id === over.id || !isDesignMode) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const activeId = active.id as string
|
|||
|
|
const overId = over.id as string
|
|||
|
|
|
|||
|
|
const activeItem = findItemById(items, activeId)
|
|||
|
|
if (!activeItem) return
|
|||
|
|
|
|||
|
|
// ⛳️ Kullanılması gereken liste: `items`
|
|||
|
|
const insertionPath = getInsertionPath(items, activeId, overId)
|
|||
|
|
if (!insertionPath) return
|
|||
|
|
|
|||
|
|
// Şimdi aktif elemanı çıkar
|
|||
|
|
let newItems = removeItemFromTree(items, activeId)
|
|||
|
|
|
|||
|
|
// ve hedef konuma ekle
|
|||
|
|
newItems = insertItemAtPath(newItems, activeItem, insertionPath)
|
|||
|
|
|
|||
|
|
// Sıra numaralarını güncelle
|
|||
|
|
const finalItems = updateOrderNumbers(newItems)
|
|||
|
|
onItemsChange(finalItems)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const renderMenuItem = (item: MenuItem, depth: number = 0): React.ReactNode => {
|
|||
|
|
return (
|
|||
|
|
<MenuItemComponent key={item.id} item={item} isDesignMode={isDesignMode} depth={depth}>
|
|||
|
|
{item.children && item.children.length > 0 && (
|
|||
|
|
<div className="ml-4">
|
|||
|
|
{item.children.map((child) => renderMenuItem(child, depth + 1))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</MenuItemComponent>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const allItems = flattenItems(items)
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<DndContext
|
|||
|
|
sensors={sensors}
|
|||
|
|
collisionDetection={closestCenter}
|
|||
|
|
onDragStart={handleDragStart}
|
|||
|
|
onDragEnd={handleDragEnd}
|
|||
|
|
>
|
|||
|
|
<SortableContext
|
|||
|
|
items={allItems.map(({ item }) => item.id)}
|
|||
|
|
strategy={verticalListSortingStrategy}
|
|||
|
|
>
|
|||
|
|
<div className="space-y-1">{items.map((item) => renderMenuItem(item))}</div>
|
|||
|
|
</SortableContext>
|
|||
|
|
|
|||
|
|
<DragOverlay>
|
|||
|
|
{activeItem ? (
|
|||
|
|
<MenuItemComponent
|
|||
|
|
item={activeItem}
|
|||
|
|
isDesignMode={isDesignMode}
|
|||
|
|
depth={0}
|
|||
|
|
isDragOverlay={true}
|
|||
|
|
/>
|
|||
|
|
) : null}
|
|||
|
|
</DragOverlay>
|
|||
|
|
</DndContext>
|
|||
|
|
)
|
|||
|
|
}
|