Skip to content

Commit 2903060

Browse files
committed
feat: draggalbe list
1 parent 082e245 commit 2903060

2 files changed

Lines changed: 105 additions & 1 deletion

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use client'
2+
3+
import React, { useState } from 'react'
4+
import { motion, AnimatePresence } from 'motion/react'
5+
import clsx from 'clsx'
6+
7+
export interface DraggableItemProps {
8+
id: string
9+
content: React.JSX.Element
10+
}
11+
12+
export interface DraggableListProps {
13+
items: DraggableItemProps[]
14+
onChange?: (items: DraggableItemProps[]) => void
15+
className?: string
16+
}
17+
18+
export const DraggableList: React.FC<DraggableListProps> = ({ items: initialItems, onChange, className }) => {
19+
const [items, setItems] = useState(initialItems)
20+
const [draggedItem, setDraggedItem] = useState<DraggableItemProps | null>(null)
21+
const [dragOverItemId, setDragOverItemId] = useState<string | number | null>(null)
22+
23+
const handleDragStart = (item: DraggableItemProps) => {
24+
setDraggedItem(item)
25+
}
26+
27+
const handleDragOver = (e: React.DragEvent, itemId: string | number) => {
28+
e.preventDefault()
29+
setDragOverItemId(itemId)
30+
}
31+
32+
const handleDragEnd = () => {
33+
if (!draggedItem || !dragOverItemId) {
34+
setDraggedItem(null)
35+
setDragOverItemId(null)
36+
return
37+
}
38+
39+
const newItems = [...items]
40+
const draggedIndex = items.findIndex((item) => item.id === draggedItem.id)
41+
const dropIndex = items.findIndex((item) => item.id === dragOverItemId)
42+
43+
newItems.splice(draggedIndex, 1)
44+
newItems.splice(dropIndex, 0, draggedItem)
45+
46+
setItems(newItems)
47+
onChange?.(newItems)
48+
setDraggedItem(null)
49+
setDragOverItemId(null)
50+
}
51+
52+
return (
53+
<div className={clsx('space-y-2', className)}>
54+
<AnimatePresence>
55+
{items.map((item) => (
56+
<motion.div
57+
key={item.id}
58+
layout
59+
initial={{ opacity: 0, y: 20 }}
60+
animate={{ opacity: 1, y: 0 }}
61+
exit={{ opacity: 0, y: -20 }}
62+
transition={{ duration: 0.2 }}
63+
draggable
64+
onDragStart={() => handleDragStart(item)}
65+
onDragOver={(e) => handleDragOver(e, item.id)}
66+
onDragEnd={handleDragEnd}
67+
className={clsx(
68+
'bg-secondary/50 border-primary/10 cursor-grab rounded-lg border p-4 shadow-sm transition-colors',
69+
dragOverItemId === item.id && 'border-orange bg-secondary/40 border-2',
70+
draggedItem?.id === item.id && 'border-2 border-gray-400 opacity-50'
71+
)}
72+
>
73+
{item.content}
74+
</motion.div>
75+
))}
76+
</AnimatePresence>
77+
</div>
78+
)
79+
}
80+
81+
export const DraggableItem: React.FC<{
82+
children: React.ReactNode
83+
className?: string
84+
}> = ({ children, className }) => {
85+
return (
86+
<div className={clsx('flex items-center gap-2', className)}>
87+
<div className="text-gray-400"></div>
88+
{children}
89+
</div>
90+
)
91+
}

src/pages/demo/index.jsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useState } from 'react'
22
import { Table } from 'antd'
33
import FixTabPanel from '@stateless/FixTabPanel'
44
import AnimationTabs from '@stateless/AnimationTabs'
@@ -17,6 +17,7 @@ import XuePng from '@assets/images/xue.png'
1717
import { Command, Cannabis, Beer, Mail } from 'lucide-react'
1818
import ScriptView from '@stateless/ScriptView'
1919
import AnimatedList from '@stateless/AnimatedList'
20+
import { DraggableList, DraggableItem } from '@stateless/DraggableList'
2021

2122
import styles from './index.module.less'
2223

@@ -97,6 +98,15 @@ const customCommandMap = {
9798
}
9899

99100
const ProDemo = () => {
101+
const [items, setItems] = useState([
102+
{ id: '1', content: <DraggableItem>First Item</DraggableItem> },
103+
{ id: '2', content: <DraggableItem>Second Item</DraggableItem> },
104+
{ id: '3', content: <DraggableItem>Third Item</DraggableItem> },
105+
])
106+
const handleReorder = (newItems) => {
107+
setItems(newItems)
108+
// Do something with the new order
109+
}
100110
return (
101111
<FixTabPanel>
102112
<ScriptView showMultiplePackageOptions={true} codeLanguage="shell" commandMap={customCommandMap} />
@@ -115,6 +125,9 @@ const ProDemo = () => {
115125
))}
116126
</AnimatedList>
117127
</section>
128+
<section className="flex items-center justify-center gap-5">
129+
<DraggableList items={items} onChange={handleReorder} className="w-[600px] max-w-md cursor-move" />
130+
</section>
118131
<StarBack />
119132
<StickyCard cards={[...Array.from({ length: 4 }, () => ({ id: Math.random() }))]} />
120133
<div className="relative w-full overflow-hidden bg-[#0a192f]">

0 commit comments

Comments
 (0)