@startupjs-ui/draggable 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/DragDropProvider/index.tsx +28 -0
- package/Draggable/index.tsx +252 -0
- package/Droppable/index.tsx +98 -0
- package/README.helpers.js +39 -0
- package/README.mdx +181 -0
- package/index.cssx.styl +3 -0
- package/index.d.ts +8 -0
- package/index.mdx.cssx.styl +15 -0
- package/index.tsx +12 -0
- package/package.json +21 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @startupjs-ui/draggable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
|
|
20
|
+
* **draggable:** refactor Draggable, Droppable, DragDropProvider components ([edf54e1](https://github.com/startupjs/startupjs-ui/commit/edf54e1171f6e89737281d580567b559d585d242))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react'
|
|
2
|
+
import { pug, $ } from 'startupjs'
|
|
3
|
+
|
|
4
|
+
export const DragDropContext = React.createContext<any>({})
|
|
5
|
+
|
|
6
|
+
export const _PropsJsonSchema = {/* DragDropProviderProps */}
|
|
7
|
+
|
|
8
|
+
export interface DragDropProviderProps {
|
|
9
|
+
/** Components rendered inside provider */
|
|
10
|
+
children?: ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function DragDropProvider ({
|
|
14
|
+
children
|
|
15
|
+
}: DragDropProviderProps): ReactNode {
|
|
16
|
+
const $context = $({
|
|
17
|
+
dropHoverId: '',
|
|
18
|
+
dragHoverIndex: null,
|
|
19
|
+
activeData: {},
|
|
20
|
+
drops: {},
|
|
21
|
+
drags: {}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return pug`
|
|
25
|
+
DragDropContext.Provider(value=$context)
|
|
26
|
+
= children
|
|
27
|
+
`
|
|
28
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useRef, type ReactNode } from 'react'
|
|
2
|
+
import { Animated, View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native'
|
|
3
|
+
import { State, PanGestureHandler } from 'react-native-gesture-handler'
|
|
4
|
+
import { pug, observer } from 'startupjs'
|
|
5
|
+
import { themed } from '@startupjs-ui/core'
|
|
6
|
+
import Portal from '@startupjs-ui/portal'
|
|
7
|
+
import { DragDropContext } from '../DragDropProvider'
|
|
8
|
+
import '../index.cssx.styl'
|
|
9
|
+
|
|
10
|
+
export const _PropsJsonSchema = {/* DraggableProps */}
|
|
11
|
+
|
|
12
|
+
export interface DraggableProps {
|
|
13
|
+
/** Content rendered inside draggable item */
|
|
14
|
+
children?: ReactNode
|
|
15
|
+
/** Custom styles applied to the draggable item */
|
|
16
|
+
style?: StyleProp<ViewStyle>
|
|
17
|
+
/** Drag type (useful for filtering drop targets) */
|
|
18
|
+
type?: string
|
|
19
|
+
/** Unique draggable item id */
|
|
20
|
+
dragId: string
|
|
21
|
+
/** @private Drop id injected by Droppable */
|
|
22
|
+
_dropId?: string
|
|
23
|
+
/** @private Index injected by Droppable */
|
|
24
|
+
_index?: number
|
|
25
|
+
/** Called when drag begins */
|
|
26
|
+
onDragBegin?: (options: {
|
|
27
|
+
dragId: string
|
|
28
|
+
dropId: string
|
|
29
|
+
dropHoverId: string
|
|
30
|
+
hoverIndex: number
|
|
31
|
+
}) => void
|
|
32
|
+
/** Called when drag ends */
|
|
33
|
+
onDragEnd?: (options: {
|
|
34
|
+
dragId: string
|
|
35
|
+
dropId: string
|
|
36
|
+
dropHoverId: string
|
|
37
|
+
hoverIndex: number
|
|
38
|
+
}) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Draggable ({
|
|
42
|
+
children,
|
|
43
|
+
style,
|
|
44
|
+
type,
|
|
45
|
+
dragId,
|
|
46
|
+
_dropId,
|
|
47
|
+
_index,
|
|
48
|
+
onDragBegin,
|
|
49
|
+
onDragEnd
|
|
50
|
+
}: DraggableProps): ReactNode {
|
|
51
|
+
const ref = useRef<any>(null)
|
|
52
|
+
const $dndContext = useContext(DragDropContext)
|
|
53
|
+
|
|
54
|
+
const animateStates = {
|
|
55
|
+
left: new Animated.Value(0),
|
|
56
|
+
top: new Animated.Value(0)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// init drags.dragId
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
$dndContext.drags[dragId].set({ ref, style: {} })
|
|
62
|
+
}, [ // eslint-disable-line react-hooks/exhaustive-deps
|
|
63
|
+
dragId,
|
|
64
|
+
_dropId,
|
|
65
|
+
_index,
|
|
66
|
+
$dndContext.drags[dragId].ref.current.get() // eslint-disable-line react-hooks/exhaustive-deps
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
if (_dropId == null || _index == null) {
|
|
70
|
+
return pug`
|
|
71
|
+
View(style=style)= children
|
|
72
|
+
`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dropId = _dropId
|
|
76
|
+
const index = _index
|
|
77
|
+
|
|
78
|
+
function onHandlerStateChange ({ nativeEvent }: any) {
|
|
79
|
+
const data = {
|
|
80
|
+
type,
|
|
81
|
+
dragId,
|
|
82
|
+
dropId,
|
|
83
|
+
dragStyle: { ...StyleSheet.flatten(style) },
|
|
84
|
+
startPosition: {
|
|
85
|
+
x: nativeEvent.x,
|
|
86
|
+
y: nativeEvent.y
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (nativeEvent.state === State.BEGAN) {
|
|
91
|
+
ref.current.measure((dragX: any, dragY: any, dragWidth: any, dragHeight: any) => {
|
|
92
|
+
data.dragStyle.height = dragHeight
|
|
93
|
+
|
|
94
|
+
$dndContext.drops[dropId].ref.current.get().measure((dx: any, dy: any, dw: any, dropHeight: any) => {
|
|
95
|
+
// init states
|
|
96
|
+
$dndContext.drags[dragId].style.set({ display: 'none' })
|
|
97
|
+
$dndContext.assign({
|
|
98
|
+
activeData: data,
|
|
99
|
+
dropHoverId: dropId,
|
|
100
|
+
dragHoverIndex: index
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
onDragBegin && onDragBegin({
|
|
104
|
+
dragId: data.dragId,
|
|
105
|
+
dropId: data.dropId,
|
|
106
|
+
dropHoverId: dropId,
|
|
107
|
+
hoverIndex: index
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (nativeEvent.state === State.END) {
|
|
114
|
+
animateStates.left.setValue(0)
|
|
115
|
+
animateStates.top.setValue(0)
|
|
116
|
+
|
|
117
|
+
onDragEnd && onDragEnd({
|
|
118
|
+
dragId: $dndContext.activeData.dragId.get(),
|
|
119
|
+
dropId: $dndContext.activeData.dropId.get(),
|
|
120
|
+
dropHoverId: $dndContext.dropHoverId.get(),
|
|
121
|
+
hoverIndex: $dndContext.dragHoverIndex.get()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// reset states
|
|
125
|
+
$dndContext.assign({
|
|
126
|
+
drags: {
|
|
127
|
+
[dragId]: { style: {} }
|
|
128
|
+
},
|
|
129
|
+
activeData: {},
|
|
130
|
+
dropHoverId: '',
|
|
131
|
+
dragHoverIndex: null
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function onGestureEvent ({ nativeEvent }: any) {
|
|
137
|
+
if (!$dndContext.dropHoverId.get()) return
|
|
138
|
+
|
|
139
|
+
animateStates.left.setValue(
|
|
140
|
+
nativeEvent.absoluteX - $dndContext.activeData.startPosition.x.get()
|
|
141
|
+
)
|
|
142
|
+
animateStates.top.setValue(
|
|
143
|
+
nativeEvent.absoluteY - $dndContext.activeData.startPosition.y.get()
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
$dndContext.activeData.x.set(nativeEvent.absoluteX)
|
|
147
|
+
$dndContext.activeData.y.set(nativeEvent.absoluteY)
|
|
148
|
+
checkPosition($dndContext.activeData.get())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function checkPosition (activeData: any) {
|
|
152
|
+
$dndContext.drops[dropId].ref.current.get().measure(async (dX: any, dY: any, dWidth: any, dHeight: any, dPageX: any, dPageY: any) => {
|
|
153
|
+
const positions: any[] = []
|
|
154
|
+
let startPosition = dPageY
|
|
155
|
+
let endPosition = dPageY
|
|
156
|
+
|
|
157
|
+
const dragsLength = $dndContext.drops[$dndContext.dropHoverId.get()].items.get()?.length || 0
|
|
158
|
+
|
|
159
|
+
for (let index = 0; index < dragsLength; index++) {
|
|
160
|
+
if (!$dndContext.dropHoverId.get()) break
|
|
161
|
+
|
|
162
|
+
const iterDragId = $dndContext.drops[$dndContext.dropHoverId.get()].items[index].get()
|
|
163
|
+
|
|
164
|
+
await new Promise<void>(resolve => {
|
|
165
|
+
const currentElement = $dndContext.drags[iterDragId].ref.current.get()
|
|
166
|
+
if (!currentElement) {
|
|
167
|
+
positions.push(null)
|
|
168
|
+
resolve()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
currentElement.measure((x: any, y: any, width: any, height: any, pageX: any, pageY: any) => {
|
|
172
|
+
if (index === 0) {
|
|
173
|
+
startPosition = dPageY
|
|
174
|
+
endPosition = dPageY + y + (height / 2)
|
|
175
|
+
} else {
|
|
176
|
+
startPosition = endPosition
|
|
177
|
+
endPosition = pageY + (height / 2)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (iterDragId === dragId) {
|
|
181
|
+
positions.push(null)
|
|
182
|
+
} else {
|
|
183
|
+
positions.push({ start: startPosition, end: endPosition })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
resolve()
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
positions.push({ start: endPosition, end: dPageY + dHeight })
|
|
192
|
+
|
|
193
|
+
for (let index = 0; index < positions.length; index++) {
|
|
194
|
+
const position = positions[index]
|
|
195
|
+
if (!position) continue
|
|
196
|
+
|
|
197
|
+
if (activeData.y > position.start && activeData.y < position.end) {
|
|
198
|
+
$dndContext.dragHoverIndex.set(index)
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const contextStyle = $dndContext.drags[dragId].style.get() || {}
|
|
206
|
+
const _style: any = StyleSheet.flatten([style, animateStates])
|
|
207
|
+
|
|
208
|
+
const isShowPlaceholder = $dndContext.activeData.get() &&
|
|
209
|
+
$dndContext.dropHoverId.get() === dropId &&
|
|
210
|
+
$dndContext.dragHoverIndex.get() === index
|
|
211
|
+
|
|
212
|
+
const isShowLastPlaceholder = $dndContext.activeData.get() &&
|
|
213
|
+
$dndContext.dropHoverId.get() === dropId &&
|
|
214
|
+
$dndContext.drops[dropId].items.get().length - 1 === index &&
|
|
215
|
+
$dndContext.dragHoverIndex.get() === index + 1
|
|
216
|
+
|
|
217
|
+
const placeholder = pug`
|
|
218
|
+
View.placeholder(
|
|
219
|
+
style={
|
|
220
|
+
height: $dndContext.activeData.get() && $dndContext.activeData.dragStyle.get() && $dndContext.activeData.dragStyle.height.get(),
|
|
221
|
+
marginTop: $dndContext.activeData.get() && $dndContext.activeData.dragStyle.get() && $dndContext.activeData.dragStyle.marginTop.get(),
|
|
222
|
+
marginBottom: $dndContext.activeData.get() && $dndContext.activeData.dragStyle.get() && $dndContext.activeData.dragStyle.marginBottom.get()
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
`
|
|
226
|
+
|
|
227
|
+
return pug`
|
|
228
|
+
if isShowPlaceholder
|
|
229
|
+
= placeholder
|
|
230
|
+
|
|
231
|
+
Portal
|
|
232
|
+
if $dndContext.activeData.dragId.get() === dragId
|
|
233
|
+
Animated.View(style=[
|
|
234
|
+
_style,
|
|
235
|
+
{ position: 'absolute', cursor: 'default' }
|
|
236
|
+
])= children
|
|
237
|
+
|
|
238
|
+
PanGestureHandler(
|
|
239
|
+
onHandlerStateChange=onHandlerStateChange
|
|
240
|
+
onGestureEvent=onGestureEvent
|
|
241
|
+
)
|
|
242
|
+
Animated.View(
|
|
243
|
+
ref=ref
|
|
244
|
+
style=[style, contextStyle]
|
|
245
|
+
)= children
|
|
246
|
+
|
|
247
|
+
if isShowLastPlaceholder
|
|
248
|
+
= placeholder
|
|
249
|
+
`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default observer(themed('Draggable', Draggable), { cache: false })
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useContext, type ReactNode } from 'react'
|
|
2
|
+
import { View, StatusBar, type StyleProp, type ViewStyle } from 'react-native'
|
|
3
|
+
import { pug, observer, $ } from 'startupjs'
|
|
4
|
+
import { themed } from '@startupjs-ui/core'
|
|
5
|
+
import { DragDropContext } from '../DragDropProvider'
|
|
6
|
+
|
|
7
|
+
export const _PropsJsonSchema = {/* DroppableProps */}
|
|
8
|
+
|
|
9
|
+
export interface DroppableProps {
|
|
10
|
+
/** Draggable items rendered inside the droppable area */
|
|
11
|
+
children?: ReactNode
|
|
12
|
+
/** Custom styles applied to the droppable container */
|
|
13
|
+
style?: StyleProp<ViewStyle>
|
|
14
|
+
/** Drop type (useful for filtering drags) */
|
|
15
|
+
type?: string
|
|
16
|
+
/** Unique droppable container id */
|
|
17
|
+
dropId: string
|
|
18
|
+
/** Called when active drag leaves this droppable */
|
|
19
|
+
onLeave?: () => void
|
|
20
|
+
/** Called when active drag enters this droppable */
|
|
21
|
+
onHover?: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Droppable ({
|
|
25
|
+
children,
|
|
26
|
+
style,
|
|
27
|
+
type,
|
|
28
|
+
dropId,
|
|
29
|
+
onLeave,
|
|
30
|
+
onHover
|
|
31
|
+
}: DroppableProps): ReactNode {
|
|
32
|
+
const ref = useRef<any>(null)
|
|
33
|
+
const $dndContext = useContext(DragDropContext)
|
|
34
|
+
const $isHover = $(false)
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
$dndContext.drops[dropId].set({
|
|
38
|
+
ref,
|
|
39
|
+
items: React.Children.map(children as any, (child: any) => {
|
|
40
|
+
return child?.props?.dragId
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}, [children, dropId, $dndContext])
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
ref.current.measure((x: any, y: any, width: any, height: any, pageX: any, pageY: any) => {
|
|
47
|
+
if (!$dndContext.activeData.dragId.get() || !$dndContext.dropHoverId.get()) {
|
|
48
|
+
$isHover.set(false)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const leftBorder = pageX
|
|
53
|
+
const rightBorder = pageX + width
|
|
54
|
+
const topBorder = pageY
|
|
55
|
+
const bottomBorder = pageY + height
|
|
56
|
+
|
|
57
|
+
const isHoverUpdate = (
|
|
58
|
+
$dndContext.activeData.x.get() > leftBorder &&
|
|
59
|
+
$dndContext.activeData.x.get() < rightBorder &&
|
|
60
|
+
$dndContext.activeData.y.get() - (StatusBar.currentHeight ?? 0) > topBorder &&
|
|
61
|
+
$dndContext.activeData.y.get() - (StatusBar.currentHeight ?? 0) < bottomBorder
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (isHoverUpdate && !$isHover.get()) {
|
|
65
|
+
$dndContext.dropHoverId.set(dropId)
|
|
66
|
+
onHover && onHover() // TODO
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isHoverUpdate && $isHover.get()) {
|
|
70
|
+
onLeave && onLeave() // TODO
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$isHover.set(isHoverUpdate)
|
|
74
|
+
})
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
}, [JSON.stringify($dndContext.activeData.get())])
|
|
77
|
+
|
|
78
|
+
const modChildren = React.Children.toArray(children).map((child: any, index) => {
|
|
79
|
+
return React.cloneElement(child, {
|
|
80
|
+
...child.props,
|
|
81
|
+
_dropId: dropId,
|
|
82
|
+
_index: index
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const hasActiveDrag = $dndContext.drops[dropId].items.get()?.includes($dndContext.activeData.dragId.get())
|
|
87
|
+
const activeStyle = hasActiveDrag ? { zIndex: 9999 } : {}
|
|
88
|
+
const contextStyle = $dndContext.drops[dropId].style.get() || {}
|
|
89
|
+
|
|
90
|
+
return pug`
|
|
91
|
+
View(
|
|
92
|
+
ref=ref
|
|
93
|
+
style=[style, activeStyle, contextStyle]
|
|
94
|
+
)= modChildren
|
|
95
|
+
`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default observer(themed('Droppable', Droppable))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { pug } from 'startupjs'
|
|
3
|
+
import Span from '@startupjs-ui/span'
|
|
4
|
+
import { Draggable, DragDropProvider, Droppable } from './index'
|
|
5
|
+
|
|
6
|
+
export function DragDropProviderSandbox ({ children, ...props }) {
|
|
7
|
+
return pug`
|
|
8
|
+
DragDropProvider(...props)
|
|
9
|
+
if children
|
|
10
|
+
= children
|
|
11
|
+
else
|
|
12
|
+
Droppable.droppable(dropId='sandbox-drop')
|
|
13
|
+
Draggable.draggable(dragId='sandbox-drag')
|
|
14
|
+
Span Drag me
|
|
15
|
+
`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DroppableSandbox (props) {
|
|
19
|
+
return pug`
|
|
20
|
+
DragDropProvider
|
|
21
|
+
Droppable.droppable(...props)
|
|
22
|
+
Draggable.draggable(dragId='a')
|
|
23
|
+
Span Item A
|
|
24
|
+
Draggable.draggable(dragId='b')
|
|
25
|
+
Span Item B
|
|
26
|
+
`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DraggableSandbox ({ children, ...props }) {
|
|
30
|
+
return pug`
|
|
31
|
+
DragDropProvider
|
|
32
|
+
Droppable.droppable(dropId='sandbox-drop')
|
|
33
|
+
Draggable.draggable(...props)
|
|
34
|
+
if children
|
|
35
|
+
= children
|
|
36
|
+
else
|
|
37
|
+
Span Drag me
|
|
38
|
+
`
|
|
39
|
+
}
|
package/README.mdx
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { pug } from 'startupjs'
|
|
3
|
+
import { Sandbox } from '@startupjs-ui/docs'
|
|
4
|
+
import {
|
|
5
|
+
Draggable,
|
|
6
|
+
DragDropProvider,
|
|
7
|
+
Droppable,
|
|
8
|
+
_PropsJsonSchema as DraggablePropsJsonSchema
|
|
9
|
+
} from './index'
|
|
10
|
+
import { _PropsJsonSchema as DragDropProviderPropsJsonSchema } from './DragDropProvider'
|
|
11
|
+
import { _PropsJsonSchema as DroppablePropsJsonSchema } from './Droppable'
|
|
12
|
+
import Span from '@startupjs-ui/span'
|
|
13
|
+
import Div from '@startupjs-ui/div'
|
|
14
|
+
import ScrollView from '@startupjs-ui/scroll-view'
|
|
15
|
+
import {
|
|
16
|
+
DragDropProviderSandbox,
|
|
17
|
+
DroppableSandbox,
|
|
18
|
+
DraggableSandbox
|
|
19
|
+
} from './README.helpers'
|
|
20
|
+
import './index.mdx.cssx.styl'
|
|
21
|
+
|
|
22
|
+
# Draggable
|
|
23
|
+
|
|
24
|
+
`Draggable` provides simple building blocks for drag & drop on React Native / RN Web:
|
|
25
|
+
|
|
26
|
+
- `DragDropProvider` stores drag/drop state and exposes it via context
|
|
27
|
+
- `Droppable` marks a drop target and tracks the order of its children
|
|
28
|
+
- `Draggable` handles gestures and renders the dragged item into a `Portal`
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
import { DragDropProvider, Droppable, Draggable } from 'startupjs-ui'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
1. Wrap the area with `DragDropProvider`.
|
|
37
|
+
2. Inside it render one or more `Droppable`s with unique `dropId`.
|
|
38
|
+
3. Render `Draggable`s inside each `Droppable`, each with a unique `dragId`.
|
|
39
|
+
4. Update your own data in `onDragEnd` using `{ dragId, dropId, dropHoverId, hoverIndex }`.
|
|
40
|
+
|
|
41
|
+
## Simple example (reorder in a single list)
|
|
42
|
+
|
|
43
|
+
```jsx example
|
|
44
|
+
const [items, setItems] = useState([
|
|
45
|
+
{ id: 'a', text: 'First' },
|
|
46
|
+
{ id: 'b', text: 'Second' },
|
|
47
|
+
{ id: 'c', text: 'Third' }
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
function onDragEnd ({ dragId, dropId, dropHoverId, hoverIndex }) {
|
|
51
|
+
if (dropId !== dropHoverId) return
|
|
52
|
+
const fromIndex = items.findIndex(i => i.id === dragId)
|
|
53
|
+
if (fromIndex === -1) return
|
|
54
|
+
|
|
55
|
+
const next = [...items]
|
|
56
|
+
const [moved] = next.splice(fromIndex, 1)
|
|
57
|
+
const toIndex = hoverIndex > fromIndex ? hoverIndex - 1 : hoverIndex
|
|
58
|
+
next.splice(toIndex, 0, moved)
|
|
59
|
+
setItems(next)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return pug`
|
|
63
|
+
DragDropProvider
|
|
64
|
+
Droppable.droppable(dropId='list')
|
|
65
|
+
each item in items
|
|
66
|
+
Draggable.draggable(
|
|
67
|
+
key=item.id
|
|
68
|
+
dragId=item.id
|
|
69
|
+
onDragEnd=onDragEnd
|
|
70
|
+
)
|
|
71
|
+
Span= item.text
|
|
72
|
+
`
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Multi-column example (move items between columns)
|
|
76
|
+
|
|
77
|
+
This example implements a small kanban-like board:
|
|
78
|
+
|
|
79
|
+
- each column is a `Droppable`
|
|
80
|
+
- each card is a `Draggable`
|
|
81
|
+
- `onDragEnd` moves the dragged card to the hovered column and index
|
|
82
|
+
|
|
83
|
+
```jsx example
|
|
84
|
+
const [data, setData] = useState([
|
|
85
|
+
{
|
|
86
|
+
id: '8c778cc6-c1dd-4dde-84a2-e3000fca8ffd',
|
|
87
|
+
title: 'Plan',
|
|
88
|
+
items: [
|
|
89
|
+
{ id: '55bd3da0-bed0-451f-8ac3-3b9a19a699fb', text: 'Quarterly newsletter and other' },
|
|
90
|
+
{ id: '8374e61f-0b95-4a4a-8282-48f9316f4157', text: 'Read a book' },
|
|
91
|
+
{ id: '13febede-112d-4d53-8e6f-a83a184e229d', text: 'Buy a new gaming laptop' },
|
|
92
|
+
{ id: '11aabadc-113d-4d53-8e6f-a83a184e229d', text: 'Buy a new gaming 2' },
|
|
93
|
+
{ id: '12febadc-112d-4d53-8e6f-a83a184e229d', text: 'Buy a new gaming 3' }
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: '2d503db0-c209-4f47-8b6a-96ef3ec6ade7',
|
|
98
|
+
title: 'WIP',
|
|
99
|
+
items: [
|
|
100
|
+
{ id: '4174c327-eea0-4ec7-96fa-2dc319c40fd6', text: 'Interview John H.', style: {} },
|
|
101
|
+
{ id: 'a2b3af0e-e482-468e-a681-3ab8303b208f', text: 'Sumbit updates to mobile storefronts', style: {} }
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'be64729e-29d8-4399-88cd-f1ee3c48b0d8',
|
|
106
|
+
title: 'Complete',
|
|
107
|
+
items: [
|
|
108
|
+
{ id: 'c539db23-77f3-49f8-93f0-2a10f1c1f1b7', text: 'Schedule meeting with Alex', style: {} },
|
|
109
|
+
{ id: 'a0d82bf2-72f4-48ba-9294-b835862519ce', text: 'Homepage refresh', style: {} }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
const arrInsertEl = (arr, index, item) => arr.splice(index, 0, item)
|
|
115
|
+
const arrRemoveEl = (arr, index) => arr.splice(index, 1)
|
|
116
|
+
|
|
117
|
+
function onDragEnd ({
|
|
118
|
+
dragId,
|
|
119
|
+
dropId,
|
|
120
|
+
dropHoverId,
|
|
121
|
+
hoverIndex
|
|
122
|
+
}) {
|
|
123
|
+
const dropIndex = data.findIndex(drop => drop.id === dropId)
|
|
124
|
+
const dropHoverIndex = data.findIndex(drop => drop.id === dropHoverId)
|
|
125
|
+
const dragIndex = data[dropIndex].items.findIndex(drag => drag.id === dragId)
|
|
126
|
+
|
|
127
|
+
const drag = data[dropIndex].items[dragIndex]
|
|
128
|
+
|
|
129
|
+
arrInsertEl(data[dropHoverIndex].items, hoverIndex, drag)
|
|
130
|
+
if (dropHoverId === dropId && hoverIndex < dragIndex) {
|
|
131
|
+
arrRemoveEl(data[dropIndex].items, dragIndex + 1)
|
|
132
|
+
} else {
|
|
133
|
+
arrRemoveEl(data[dropIndex].items, dragIndex)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setData([...data])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return pug`
|
|
140
|
+
DragDropProvider
|
|
141
|
+
ScrollView(horizontal=true)
|
|
142
|
+
Div(row)
|
|
143
|
+
each drop in data
|
|
144
|
+
Droppable.droppable(
|
|
145
|
+
key=drop.id
|
|
146
|
+
dropId=drop.id
|
|
147
|
+
)
|
|
148
|
+
each drag in drop.items
|
|
149
|
+
Draggable.draggable(
|
|
150
|
+
key=drag.id
|
|
151
|
+
dragId=drag.id
|
|
152
|
+
onDragEnd=onDragEnd
|
|
153
|
+
)
|
|
154
|
+
Span= drag.text
|
|
155
|
+
`
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Sandbox
|
|
159
|
+
|
|
160
|
+
### DragDropProvider
|
|
161
|
+
|
|
162
|
+
<Sandbox
|
|
163
|
+
Component={DragDropProviderSandbox}
|
|
164
|
+
propsJsonSchema={DragDropProviderPropsJsonSchema}
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
### Droppable
|
|
168
|
+
|
|
169
|
+
<Sandbox
|
|
170
|
+
Component={DroppableSandbox}
|
|
171
|
+
props={{ dropId: 'sandbox-drop' }}
|
|
172
|
+
propsJsonSchema={DroppablePropsJsonSchema}
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
### Draggable
|
|
176
|
+
|
|
177
|
+
<Sandbox
|
|
178
|
+
Component={DraggableSandbox}
|
|
179
|
+
props={{ dragId: 'sandbox-drag' }}
|
|
180
|
+
propsJsonSchema={DraggablePropsJsonSchema}
|
|
181
|
+
/>
|
package/index.cssx.styl
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
|
|
3
|
+
|
|
4
|
+
export { default as DragDropProvider, DragDropContext, type DragDropProviderProps } from './DragDropProvider';
|
|
5
|
+
export { default as Draggable, type DraggableProps, _PropsJsonSchema } from './Draggable';
|
|
6
|
+
export { default as Droppable, type DroppableProps } from './Droppable';
|
|
7
|
+
export { _PropsJsonSchema as DragDropProviderPropsJsonSchema } from './DragDropProvider';
|
|
8
|
+
export { _PropsJsonSchema as DroppablePropsJsonSchema } from './Droppable';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.droppable
|
|
2
|
+
margin-right 2u
|
|
3
|
+
background-color var(--color-bg-main-subtle)
|
|
4
|
+
padding 1u
|
|
5
|
+
|
|
6
|
+
.draggable
|
|
7
|
+
width 200px
|
|
8
|
+
background-color var(--color-bg-main-strong)
|
|
9
|
+
margin-top .5u
|
|
10
|
+
margin-bottom .5u
|
|
11
|
+
padding 1u 2u
|
|
12
|
+
border-width 1px
|
|
13
|
+
border-style solid
|
|
14
|
+
border-color var(--color-border-main)
|
|
15
|
+
radius()
|
package/index.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
default as DragDropProvider,
|
|
3
|
+
DragDropContext,
|
|
4
|
+
type DragDropProviderProps
|
|
5
|
+
} from './DragDropProvider'
|
|
6
|
+
|
|
7
|
+
export { default as Draggable, type DraggableProps, _PropsJsonSchema } from './Draggable'
|
|
8
|
+
|
|
9
|
+
export { default as Droppable, type DroppableProps } from './Droppable'
|
|
10
|
+
|
|
11
|
+
export { _PropsJsonSchema as DragDropProviderPropsJsonSchema } from './DragDropProvider'
|
|
12
|
+
export { _PropsJsonSchema as DroppablePropsJsonSchema } from './Droppable'
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@startupjs-ui/draggable",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"main": "index.tsx",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@startupjs-ui/core": "^0.1.3",
|
|
12
|
+
"@startupjs-ui/portal": "^0.1.3"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": "*",
|
|
16
|
+
"react-native": "*",
|
|
17
|
+
"react-native-gesture-handler": "*",
|
|
18
|
+
"startupjs": "*"
|
|
19
|
+
},
|
|
20
|
+
"gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
|
|
21
|
+
}
|