| @@ -1,4 +1,8 @@ | |||||
| import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' | |||||
| import { DndContext, | |||||
| PointerSensor, | |||||
| useDroppable, | |||||
| useSensor, | |||||
| useSensors } from '@dnd-kit/core' | |||||
| import { AnimatePresence, motion } from 'framer-motion' | import { AnimatePresence, motion } from 'framer-motion' | ||||
| import { useEffect, useRef, useState } from 'react' | import { useEffect, useRef, useState } from 'react' | ||||
| @@ -168,6 +172,44 @@ const findTag = ( | |||||
| } | } | ||||
| const detachEdge = ( | |||||
| nodes: Tag[], | |||||
| parentId: number, | |||||
| childId: number, | |||||
| ): Tag[] => nodes.map (t => { | |||||
| if (t.id === parentId) | |||||
| { | |||||
| const children = (t.children ?? []).filter (c => c.id !== childId) | |||||
| return { ...t, children } | |||||
| } | |||||
| return t.children ? { ...t, children: detachEdge (t.children, parentId, childId) } : t | |||||
| }) | |||||
| const insertRootAt = ( | |||||
| roots: Tag[], | |||||
| index: number, | |||||
| tag: Tag, | |||||
| ): Tag[] => { | |||||
| const without = roots.filter (t => t.id !== tag.id) | |||||
| const next = without.slice () | |||||
| next.splice (Math.min (Math.max (index, 0), next.length), 0, tag) | |||||
| return next | |||||
| } | |||||
| const DropSlot = ({ cat, index }: { cat: Category, index: number }) => { | |||||
| const { setNodeRef, isOver: over } = useDroppable ({ | |||||
| id: `slot:${ cat }:${ index }`, | |||||
| data: { kind: 'slot', cat, index } }) | |||||
| return ( | |||||
| <li ref={setNodeRef} className="h-1"> | |||||
| {over && <div className="h-0.5 w-full rounded bg-sky-400"/>} | |||||
| </li>) | |||||
| } | |||||
| type Props = { post: Post | null } | type Props = { post: Post | null } | ||||
| @@ -180,26 +222,58 @@ export default (({ post }: Props) => { | |||||
| useSensor (PointerSensor, { activationConstraint: { distance: 6 } })) | useSensor (PointerSensor, { activationConstraint: { distance: 6 } })) | ||||
| const onDragEnd = async (e: DragEndEvent) => { | const onDragEnd = async (e: DragEndEvent) => { | ||||
| const activeKind = e.active.data.current?.kind | |||||
| if (activeKind !== 'tag') | |||||
| return | |||||
| const childId: number | undefined = e.active.data.current?.tagId | const childId: number | undefined = e.active.data.current?.tagId | ||||
| const parentId: number | undefined = e.over?.data.current?.tagId | |||||
| const fromParentId: number | undefined = e.active.data.current?.parentTagId | |||||
| if (!(childId) || !(parentId)) | |||||
| return | |||||
| const overKind = e.over?.data.current?.kind | |||||
| if (childId === parentId) | |||||
| if (!(childId) || !(overKind)) | |||||
| return | return | ||||
| setTags (prev => { | |||||
| const child = findTag (prev, childId) | |||||
| const parent = findTag (prev, parentId) | |||||
| if (!(child) || !(parent)) | |||||
| return prev | |||||
| if (overKind === 'tag') | |||||
| { | |||||
| const parentId: number | undefined = e.over?.data.current?.tagId | |||||
| if (!(parentId) || childId === parentId) | |||||
| return | |||||
| setTags (prev => { | |||||
| const child = findTag (prev, childId) | |||||
| const parent = findTag (prev, parentId) | |||||
| if (!(child) || !(parent) || isDescendant (child, parentId)) | |||||
| return prev | |||||
| return attachChildOptimistic (prev, parentId, childId) | |||||
| }) | |||||
| if (isDescendant (child, parentId)) | |||||
| return prev | |||||
| return | |||||
| } | |||||
| return attachChildOptimistic (prev, parentId, childId) | |||||
| }) | |||||
| if (overKind === 'slot') | |||||
| { | |||||
| const cat: Category | undefined = e.over?.data.current?.cat | |||||
| const index: number | undefined = e.over?.data.current?.index | |||||
| if (!(cat) || index == null) | |||||
| return | |||||
| setTags (prev => { | |||||
| const child = findTag (prev, childId) | |||||
| if (!(child)) | |||||
| return prev | |||||
| const next: TagByCategory = { ...prev } | |||||
| if (fromParentId) | |||||
| next[cat] = detachEdge (next[cat], fromParentId, childId) as any | |||||
| next[cat] = insertRootAt (next[cat], index, child) | |||||
| return next | |||||
| }) | |||||
| } | |||||
| } | } | ||||
| const categoryNames: Record<Category, string> = { | const categoryNames: Record<Category, string> = { | ||||
| @@ -260,7 +334,10 @@ export default (({ post }: Props) => { | |||||
| <motion.ul layout> | <motion.ul layout> | ||||
| <AnimatePresence initial={false}> | <AnimatePresence initial={false}> | ||||
| {tags[cat].map (tag => ( | {tags[cat].map (tag => ( | ||||
| renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)))} | |||||
| <> | |||||
| {renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)} | |||||
| </>))} | |||||
| <DropSlot cat={cat} index={0}/> | |||||
| </AnimatePresence> | </AnimatePresence> | ||||
| </motion.ul> | </motion.ul> | ||||
| </motion.div>))} | </motion.div>))} | ||||