このコミットが含まれているのは:
@@ -14,10 +14,8 @@ 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 [preview, setPreview] = useState<string[] | null> (null)
|
||||
const [active, setActive] = useState<string | null> (null)
|
||||
const latest = useRef<string[] | null> (null)
|
||||
const lastOver = useRef<string | null> (null)
|
||||
const drag = useRef<{ id: string; ids: string[] } | null> (null)
|
||||
const motion = useClientMotion ()
|
||||
const sensors = useSensors (
|
||||
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
|
||||
@@ -27,12 +25,11 @@ const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
|
||||
if (fixed.includes (from) || fixed.includes (to)) return current
|
||||
const source = current.indexOf (from)
|
||||
const target = current.indexOf (to)
|
||||
return source < 0 || target < 0 ? current : arrayMove (current, source, target)
|
||||
return source < 0 || target < 0 || source === target
|
||||
? current : arrayMove (current, source, target)
|
||||
}
|
||||
const reset = () => {
|
||||
latest.current = null
|
||||
lastOver.current = null
|
||||
setPreview (null)
|
||||
drag.current = null
|
||||
setActive (null)
|
||||
}
|
||||
return <DndContext sensors={sensors} collisionDetection={closestCenter}
|
||||
@@ -43,28 +40,20 @@ const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
|
||||
onDragCancel: () => undefined },
|
||||
screenReaderInstructions: { draggable: active == null ? '' : name (active).text } }}
|
||||
onDragStart={event => {
|
||||
latest.current = ids
|
||||
setPreview (ids)
|
||||
setActive (String (event.active.id))
|
||||
}} onDragOver={event => {
|
||||
if (event.over == null || event.active.id === event.over.id) return
|
||||
const target = String (event.over.id)
|
||||
if (target === lastOver.current) return
|
||||
latest.current = move (latest.current ?? ids, String (event.active.id), target)
|
||||
lastOver.current = target
|
||||
setPreview (latest.current)
|
||||
const id = String (event.active.id)
|
||||
drag.current = { id, ids }
|
||||
setActive (id)
|
||||
}} onDragCancel={reset} onDragEnd={event => {
|
||||
if (event.over != null)
|
||||
{
|
||||
const target = String (event.over.id)
|
||||
const current = latest.current ?? ids
|
||||
onChange (target === lastOver.current || target === String (event.active.id)
|
||||
? current : move (current, String (event.active.id), target))
|
||||
}
|
||||
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={preview ?? ids} strategy={horizontalListSortingStrategy}>
|
||||
{children (preview ?? ids)}
|
||||
<SortableContext items={ids} strategy={horizontalListSortingStrategy}>
|
||||
{children (ids)}
|
||||
</SortableContext>
|
||||
{typeof document !== 'undefined' && createPortal (<DragOverlay
|
||||
dropAnimation={motion.reduced ? null : {
|
||||
|
||||
@@ -46,6 +46,8 @@ const SortableItem = ({ id, name, fixed, children, moveGroup, handleId, renderSh
|
||||
return <div ref={item.setNodeRef} style={style}
|
||||
className={cn ('relative max-w-full rounded border border-slate-300 bg-white',
|
||||
'text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100',
|
||||
item.isOver && !item.isDragging
|
||||
&& 'ring-2 ring-blue-500 dark:ring-blue-400',
|
||||
item.isDragging && 'opacity-30 ring-2 ring-blue-500 dark:ring-blue-400')}>
|
||||
<div className="flex min-w-0 items-start gap-1 px-1">
|
||||
{handle}
|
||||
|
||||
@@ -15,6 +15,12 @@ type Groups = Record<string, string[]>
|
||||
type Name = { text: string; language: string }
|
||||
|
||||
|
||||
const sameGroups = (left: Groups, right: Groups): boolean =>
|
||||
Object.keys (left).length === Object.keys (right).length
|
||||
&& Object.keys (left).every (key => left[key].length === right[key]?.length
|
||||
&& left[key].every ((id, index) => id === right[key][index]))
|
||||
|
||||
|
||||
const Group = ({ id, children }: { id: string; children: ReactNode }) => {
|
||||
const drop = useDroppable ({ id: `group:${id}` })
|
||||
return <div ref={drop.setNodeRef} className={cn (
|
||||
@@ -29,13 +35,10 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
|
||||
renderGroup: (id: string, content: ReactNode) => ReactNode
|
||||
onChange: (groups: Groups) => void; fixed?: string[] },
|
||||
) => {
|
||||
const [preview, setPreview] = useState<Groups | null> (null)
|
||||
const previewRef = useRef<Groups | null> (null)
|
||||
const lastOver = useRef<string | null> (null)
|
||||
const drag = useRef<{ id: string; groups: Groups } | null> (null)
|
||||
const scope = useId ()
|
||||
const [active, setActive] = useState<string | null> (null)
|
||||
const motion = useClientMotion ()
|
||||
const current = preview ?? groups
|
||||
const sensors = useSensors (
|
||||
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
|
||||
useSensor (TouchSensor, { activationConstraint: { delay: 350, tolerance: 8 } }),
|
||||
@@ -45,7 +48,8 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
|
||||
const relocate = (value: Groups, id: string, over: string): Groups => {
|
||||
const source = owner (id, value)
|
||||
const target = over.startsWith ('group:') ? over.slice (6) : owner (over, value)
|
||||
if (source == null || target == null || fixed.includes (id) || fixed.includes (over))
|
||||
if (source == null || target == null || value[target] == null
|
||||
|| fixed.includes (id) || fixed.includes (over))
|
||||
return value
|
||||
if (source === target)
|
||||
{
|
||||
@@ -62,19 +66,20 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
|
||||
[target]: destination }
|
||||
}
|
||||
const moveGroup = (id: string, direction: number) => {
|
||||
if (drag.current != null) return
|
||||
const keys = Object.keys (groups)
|
||||
const group = owner (id, groups)
|
||||
const target = keys[keys.indexOf (group ?? '') + direction]
|
||||
if (target != null)
|
||||
{
|
||||
onChange (relocate (groups, id, `group:${target}`))
|
||||
const next = relocate (groups, id, `group:${target}`)
|
||||
if (sameGroups (groups, next)) return
|
||||
onChange (next)
|
||||
requestAnimationFrame (() => document.getElementById (`${scope}-${id}`)?.focus ())
|
||||
}
|
||||
}
|
||||
const reset = () => {
|
||||
previewRef.current = null
|
||||
lastOver.current = null
|
||||
setPreview (null)
|
||||
drag.current = null
|
||||
setActive (null)
|
||||
}
|
||||
return <DndContext sensors={sensors} collisionDetection={closestCenter}
|
||||
@@ -87,32 +92,20 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
|
||||
onDragCancel: () => undefined },
|
||||
screenReaderInstructions: { draggable: active == null ? '' : name (active).text } }}
|
||||
onDragStart={event => {
|
||||
previewRef.current = groups
|
||||
lastOver.current = null
|
||||
setPreview (groups)
|
||||
setActive (String (event.active.id))
|
||||
}} onDragOver={event => {
|
||||
if (event.over == null || event.active.id === event.over.id) return
|
||||
const target = String (event.over.id)
|
||||
if (lastOver.current === target) return
|
||||
const next = relocate (previewRef.current ?? groups, String (event.active.id), target)
|
||||
previewRef.current = next
|
||||
lastOver.current = target
|
||||
setPreview (next)
|
||||
const id = String (event.active.id)
|
||||
drag.current = { id, groups }
|
||||
setActive (id)
|
||||
}} onDragCancel={reset} onDragEnd={event => {
|
||||
if (event.over != null)
|
||||
{
|
||||
const id = String (event.active.id)
|
||||
const target = String (event.over.id)
|
||||
const latest = previewRef.current ?? groups
|
||||
const next = (target === id || target === lastOver.current)
|
||||
? latest : relocate (latest, id, target)
|
||||
onChange (next)
|
||||
}
|
||||
const session = drag.current
|
||||
reset ()
|
||||
if (session == null || event.over == null || session.id !== String (event.active.id)
|
||||
|| !sameGroups (session.groups, groups)) return
|
||||
const next = relocate (groups, session.id, String (event.over.id))
|
||||
if (!sameGroups (groups, next)) onChange (next)
|
||||
}}>
|
||||
|
||||
{Object.entries (current).map (([group, ids]) => <div key={group}>
|
||||
{/* Keep indexes and droppable geometry stable until the drop is committed. */}
|
||||
{Object.entries (groups).map (([group, ids]) => <div key={group}>
|
||||
{renderGroup (group, <SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||
<Group id={group}>{ids.map (id => <SortableItem key={id} id={id}
|
||||
name={name (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする