erp-platform/ui/src/views/menu/SortableMenuTree.tsx

233 lines
6.1 KiB
TypeScript
Raw Normal View History

2025-06-26 13:58:53 +00:00
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 || `temp-${Math.random().toString(36).substr(2, 9)}`}
item={item}
isDesignMode={isDesignMode}
depth={depth}
>
2025-06-26 13:58:53 +00:00
{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>
)
}