75 行
3.6 KiB
TypeScript
75 行
3.6 KiB
TypeScript
import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor,
|
|
closestCenter, useSensor, useSensors } from '@dnd-kit/core'
|
|
import { SortableContext, arrayMove, horizontalListSortingStrategy,
|
|
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
|
import { useRef, useState } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { CSS } from '@dnd-kit/utilities'
|
|
|
|
import { useClientMotion } from '@/lib/useClientMotion'
|
|
|
|
import type { ReactNode } from 'react'
|
|
|
|
|
|
const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
|
|
ids: string[]; fixed?: string[]; name: (id: string) => { text: string; language: string }
|
|
onChange: (ids: string[]) => void; children: (ids: string[]) => ReactNode },
|
|
) => {
|
|
const [active, setActive] = useState<string | null> (null)
|
|
const drag = useRef<{ id: string; ids: string[] } | null> (null)
|
|
const motion = useClientMotion ()
|
|
const sensors = useSensors (
|
|
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
|
|
useSensor (TouchSensor, { activationConstraint: { delay: 350, tolerance: 8 } }),
|
|
useSensor (KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }))
|
|
const move = (current: string[], from: string, to: string) => {
|
|
if (fixed.includes (from) || fixed.includes (to)) return current
|
|
const source = current.indexOf (from)
|
|
const target = current.indexOf (to)
|
|
return source < 0 || target < 0 || source === target
|
|
? current : arrayMove (current, source, target)
|
|
}
|
|
const reset = () => {
|
|
drag.current = null
|
|
setActive (null)
|
|
}
|
|
return <DndContext sensors={sensors} collisionDetection={closestCenter}
|
|
accessibility={{ announcements: {
|
|
onDragStart: event => name (String (event.active.id)).text,
|
|
onDragOver: event => event.over == null ? undefined : name (String (event.over.id)).text,
|
|
onDragMove: () => undefined, onDragEnd: () => undefined,
|
|
onDragCancel: () => undefined },
|
|
screenReaderInstructions: { draggable: active == null ? '' : name (active).text } }}
|
|
onDragStart={event => {
|
|
const id = String (event.active.id)
|
|
drag.current = { id, ids }
|
|
setActive (id)
|
|
}} onDragCancel={reset} onDragEnd={event => {
|
|
const session = drag.current
|
|
reset ()
|
|
if (session == null || event.over == null || session.id !== String (event.active.id)
|
|
|| session.ids.length !== ids.length
|
|
|| session.ids.some ((id, index) => id !== ids[index])) return
|
|
const next = move (ids, session.id, String (event.over.id))
|
|
if (next.some ((id, index) => id !== ids[index])) onChange (next)
|
|
}}>
|
|
<SortableContext items={ids} strategy={horizontalListSortingStrategy}>
|
|
{children (ids)}
|
|
</SortableContext>
|
|
{typeof document !== 'undefined' && createPortal (<DragOverlay adjustScale={false}
|
|
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
|
dropAnimation={motion.reduced ? null : {
|
|
duration: motion.duration ('drop'), easing: motion.easing,
|
|
keyframes: ({ transform }) => [
|
|
{ transform: CSS.Translate.toString (transform.initial) },
|
|
{ transform: CSS.Translate.toString (transform.final) }] }}>
|
|
{active != null && <div lang={name (active).language}
|
|
className="w-max max-w-[min(20rem,calc(100vw_-_2rem))] truncate rounded
|
|
border border-blue-500 bg-white px-2 py-1 text-sm text-slate-900 shadow-lg
|
|
dark:border-blue-400 dark:bg-slate-800 dark:text-slate-100">{name (active).text}</div>}
|
|
</DragOverlay>, document.body)}
|
|
</DndContext>
|
|
}
|
|
|
|
export default SortableColumns
|