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
417 lines
9.6 KiB
TypeScript
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 } |