Files
MikrotikManager/components/reui/sortable.tsx
T
Denozordec a1a9124f3d
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
fix(config): добавить новые зависимости и обновить конфигурацию компонентов
2026-06-30 21:30:40 +07:00

417 lines
9.6 KiB
TypeScript

"use client"
import * as React from "react"
import {
Children,
cloneElement,
createContext,
CSSProperties,
isValidElement,
ReactElement,
ReactNode,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useState,
} from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import {
defaultDropAnimationSideEffects,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
DropAnimation,
KeyboardSensor,
MeasuringStrategy,
Modifiers,
MouseSensor,
TouchSensor,
UniqueIdentifier,
useSensor,
useSensors,
type DraggableSyntheticListeners,
} from "@dnd-kit/core"
import {
arrayMove,
defaultAnimateLayoutChanges,
rectSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
type AnimateLayoutChanges,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { createPortal } from "react-dom"
import { cn } from "@/lib/utils"
// Sortable Item Context
const SortableItemContext = createContext<{
listeners: DraggableSyntheticListeners | undefined
isDragging?: boolean
disabled?: boolean
}>({
listeners: undefined,
isDragging: false,
disabled: false,
})
const IsOverlayContext = createContext(false)
const SortableInternalContext = createContext<{
activeId: UniqueIdentifier | null
modifiers?: Modifiers
}>({
activeId: null,
modifiers: undefined,
})
const animateLayoutChanges: AnimateLayoutChanges = (args) =>
defaultAnimateLayoutChanges({ ...args, wasDragging: true })
const dropAnimationConfig: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: "0.4",
},
},
}),
}
// Multipurpose Sortable Component
export interface SortableRootProps<T> extends Omit<
useRender.ComponentProps<"div">,
"onDragStart" | "onDragEnd" | "children"
> {
value: T[]
onValueChange: (value: T[]) => void
getItemValue: (item: T) => string
children: ReactNode
onMove?: (event: {
event: DragEndEvent
activeIndex: number
overIndex: number
}) => void
strategy?: "horizontal" | "vertical" | "grid"
onDragStart?: (event: DragStartEvent) => void
onDragEnd?: (event: DragEndEvent) => void
modifiers?: Modifiers
}
function Sortable<T>({
value,
onValueChange,
getItemValue,
className,
render,
onMove,
strategy = "vertical",
onDragStart,
onDragEnd,
modifiers,
children,
...props
}: SortableRootProps<T>) {
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null)
const [mounted, setMounted] = useState(false)
useLayoutEffect(() => setMounted(true), [])
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint: {
distance: 10,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 5,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
const handleDragStart = useCallback(
(event: DragStartEvent) => {
setActiveId(event.active.id)
onDragStart?.(event)
},
[onDragStart]
)
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event
setActiveId(null)
onDragEnd?.(event)
if (!over) return
// Handle item reordering
const activeIndex = value.findIndex(
(item: T) => getItemValue(item) === active.id
)
const overIndex = value.findIndex(
(item: T) => getItemValue(item) === over.id
)
if (activeIndex !== overIndex) {
if (onMove) {
onMove({ event, activeIndex, overIndex })
} else {
const newValue = arrayMove(value, activeIndex, overIndex)
onValueChange(newValue)
}
}
},
[value, getItemValue, onValueChange, onMove, onDragEnd]
)
const handleDragCancel = useCallback(() => {
setActiveId(null)
}, [])
const getStrategy = () => {
switch (strategy) {
case "horizontal":
return rectSortingStrategy
case "grid":
return rectSortingStrategy
case "vertical":
default:
return verticalListSortingStrategy
}
}
const itemIds = useMemo(() => value.map(getItemValue), [value, getItemValue])
const contextValue = useMemo(
() => ({ activeId, modifiers }),
[activeId, modifiers]
)
const defaultProps = {
"data-slot": "sortable",
"data-dragging": activeId !== null,
className: cn(activeId !== null && "cursor-grabbing!", className),
children,
}
// Find the active child for the overlay
const overlayContent = useMemo(() => {
if (!activeId) return null
let result: ReactNode = null
Children.forEach(children, (child) => {
if (isValidElement(child) && (child.props as any).value === activeId) {
result = cloneElement(child as ReactElement<any>, {
...(child.props as any),
className: cn((child.props as any).className, "z-50"),
})
}
})
return result
}, [activeId, children])
return (
<SortableInternalContext.Provider value={contextValue}>
<DndContext
sensors={sensors}
modifiers={modifiers}
measuring={{
droppable: {
strategy: MeasuringStrategy.Always,
},
}}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext items={itemIds} strategy={getStrategy()}>
{useRender({
defaultTagName: "div",
render,
props: mergeProps<"div">(defaultProps, props),
})}
</SortableContext>
{mounted &&
createPortal(
<DragOverlay
dropAnimation={dropAnimationConfig}
modifiers={modifiers}
className={cn("z-50", activeId && "cursor-grabbing")}
>
<IsOverlayContext.Provider value={true}>
{overlayContent}
</IsOverlayContext.Provider>
</DragOverlay>,
document.body
)}
</DndContext>
</SortableInternalContext.Provider>
)
}
export interface SortableItemProps extends useRender.ComponentProps<"div"> {
value: string
disabled?: boolean
}
function SortableItem({
value,
className,
render,
disabled,
...props
}: SortableItemProps) {
const isOverlay = useContext(IsOverlayContext)
const {
setNodeRef,
transform,
transition,
attributes,
listeners,
isDragging: isSortableDragging,
} = useSortable({
id: value,
disabled: disabled || isOverlay,
animateLayoutChanges,
})
if (isOverlay) {
const defaultProps = {
"data-slot": "sortable-item",
"data-value": value,
"data-dragging": true,
className: cn(className),
children: props.children,
}
return (
<SortableItemContext.Provider
value={{ listeners: undefined, isDragging: true, disabled: false }}
>
{useRender({
defaultTagName: "div",
render,
props: mergeProps<"div">(defaultProps, props),
})}
</SortableItemContext.Provider>
)
}
const style = {
transition,
transform: CSS.Transform.toString(transform),
} as CSSProperties
const defaultProps = {
"data-slot": "sortable-item",
"data-value": value,
"data-dragging": isSortableDragging,
"data-disabled": disabled,
ref: setNodeRef,
style,
...attributes,
className: cn(
isSortableDragging && "opacity-50 z-50",
disabled && "opacity-50",
className
),
children: props.children,
}
return (
<SortableItemContext.Provider
value={{ listeners, isDragging: isSortableDragging, disabled }}
>
{useRender({
defaultTagName: "div",
render,
props: mergeProps<"div">(defaultProps, props),
})}
</SortableItemContext.Provider>
)
}
export interface SortableItemHandleProps extends useRender.ComponentProps<"div"> {
cursor?: boolean
}
function SortableItemHandle({
className,
render,
cursor = true,
...props
}: SortableItemHandleProps) {
const { listeners, isDragging, disabled } = useContext(SortableItemContext)
const defaultProps = {
"data-slot": "sortable-item-handle",
"data-dragging": isDragging,
"data-disabled": disabled,
...listeners,
className: cn(
cursor && (isDragging ? "cursor-grabbing!" : "cursor-grab!"),
className
),
children: props.children,
}
return useRender({
defaultTagName: "div",
render,
props: mergeProps<"div">(defaultProps, props),
})
}
export interface SortableOverlayProps extends Omit<
React.ComponentProps<typeof DragOverlay>,
"children"
> {
children?: ReactNode | ((params: { value: UniqueIdentifier }) => ReactNode)
}
function SortableOverlay({
children,
className,
...props
}: SortableOverlayProps) {
const { activeId, modifiers } = useContext(SortableInternalContext)
const [mounted, setMounted] = useState(false)
useLayoutEffect(() => setMounted(true), [])
const content =
activeId && children
? typeof children === "function"
? children({ value: activeId })
: children
: null
if (!mounted) return null
return createPortal(
<DragOverlay
dropAnimation={dropAnimationConfig}
modifiers={modifiers}
className={cn("z-50", activeId && "cursor-grabbing", className)}
{...props}
>
<IsOverlayContext.Provider value={true}>
{content}
</IsOverlayContext.Provider>
</DragOverlay>,
document.body
)
}
export { Sortable, SortableItem, SortableItemHandle, SortableOverlay }