@octane-xplat/dnd-kit 0.0.1 → 0.11.0
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/LICENSE +21 -0
- package/README.md +306 -2
- package/dist/native/components.js +275 -0
- package/dist/native/controller.js +236 -0
- package/dist/native/geometry.js +18 -0
- package/dist/native/index.js +4 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/native/sort.js +11 -0
- package/dist/web/components.js +215 -0
- package/dist/web/controller.js +236 -0
- package/dist/web/geometry.web.js +16 -0
- package/dist/web/index.js +4 -0
- package/dist/web/octane-client-build.json +8 -0
- package/dist/web/sort.js +11 -0
- package/examples/sortable.macos.tsx +128 -0
- package/examples/sortable.tsx +22 -0
- package/package.json +83 -3
- package/src/abort.macos.ts +7 -0
- package/src/components.tsx +262 -0
- package/src/components.web.test.tsx +118 -0
- package/src/controller.mobile.test.ts +176 -0
- package/src/controller.ts +301 -0
- package/src/geometry.macos.test.ts +34 -0
- package/src/geometry.macos.ts +32 -0
- package/src/geometry.ts +18 -0
- package/src/geometry.web.ts +14 -0
- package/src/index.macos.ts +23 -0
- package/src/index.ts +21 -0
- package/src/index.web.ts +21 -0
- package/src/props.ts +83 -0
- package/src/sort.ts +19 -0
- package/types/generated/.tsrx-typegen-manifest.json +11 -0
- package/types/generated/components.d.ts +13 -0
- package/types/generated/controller.d.ts +32 -0
- package/types/generated/geometry.web.d.ts +3 -0
- package/types/generated/index.d.ts +5 -0
- package/types/generated/props.d.ts +84 -0
- package/types/generated/sort.d.ts +2 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { DragDropManager, Draggable, Droppable, Sensor } from '@dnd-kit/abstract'
|
|
2
|
+
import { defaultCollisionDetection } from '@dnd-kit/collision'
|
|
3
|
+
import { batch, effect, signal, untracked } from '@dnd-kit/state'
|
|
4
|
+
import type { Rectangle } from '@dnd-kit/geometry'
|
|
5
|
+
import type { PanEvent } from '@octane-xplat/ui'
|
|
6
|
+
import type {
|
|
7
|
+
DndContextProps,
|
|
8
|
+
DndId,
|
|
9
|
+
DndSnapshot,
|
|
10
|
+
DragEvent,
|
|
11
|
+
DraggableOptions,
|
|
12
|
+
DroppableOptions,
|
|
13
|
+
} from './props'
|
|
14
|
+
|
|
15
|
+
import { measure } from './geometry'
|
|
16
|
+
|
|
17
|
+
class PanDraggable extends Draggable {
|
|
18
|
+
onPan?: (event: PanEvent) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Xplat View supplies normalized pan input; the abstract core owns the drag lifecycle. */
|
|
22
|
+
class PanSensor extends Sensor {
|
|
23
|
+
bind(input: Draggable) {
|
|
24
|
+
const source = input as PanDraggable
|
|
25
|
+
let origin: Rectangle | null = null
|
|
26
|
+
let bound = true
|
|
27
|
+
let base = { x: 0, y: 0 }
|
|
28
|
+
let queue = Promise.resolve()
|
|
29
|
+
const handle = async (event: PanEvent) => {
|
|
30
|
+
const manager = this.manager as DndController
|
|
31
|
+
if (!bound) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (event.state === 'began') {
|
|
36
|
+
if (source.disabled || !manager.dragOperation.status.idle) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
origin = measure(manager.draggableNodes.get(source.id))
|
|
41
|
+
if (!origin) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
base = { x: event.dx, y: event.dy }
|
|
46
|
+
manager.scrollShift$.value = { x: 0, y: 0 }
|
|
47
|
+
manager.actions.start({ source, coordinates: origin.center })
|
|
48
|
+
manager.dragOperation.shape = origin
|
|
49
|
+
manager.refresh()
|
|
50
|
+
await manager.renderer.rendering
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
!origin ||
|
|
56
|
+
manager.dragOperation.source?.id !== source.id ||
|
|
57
|
+
!manager.dragOperation.status.dragging
|
|
58
|
+
) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const x = event.dx - base.x
|
|
63
|
+
const y = event.dy - base.y
|
|
64
|
+
manager.actions.move({ to: { x: origin.center.x + x, y: origin.center.y + y } })
|
|
65
|
+
// The core commits movement in a microtask. Serialize release after it.
|
|
66
|
+
await Promise.resolve()
|
|
67
|
+
manager.dragOperation.shape = origin.translate(x, y)
|
|
68
|
+
manager.refresh()
|
|
69
|
+
if (event.state === 'ended' || event.state === 'cancelled') {
|
|
70
|
+
manager.actions.stop({ canceled: event.state === 'cancelled' })
|
|
71
|
+
origin = null
|
|
72
|
+
} else {
|
|
73
|
+
manager.startAutoScroll()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
source.onPan = (event) => {
|
|
78
|
+
queue = queue
|
|
79
|
+
.then(() => handle(event))
|
|
80
|
+
.catch((error) => {
|
|
81
|
+
origin = null
|
|
82
|
+
|
|
83
|
+
;(this.manager as DndController).cancel()
|
|
84
|
+
console.error('[dnd-kit] pan failed', error)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
bound = false
|
|
90
|
+
source.onPan = undefined
|
|
91
|
+
origin = null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One controller per DndContext; no global registry or cross-root context assumptions. */
|
|
97
|
+
export class DndController extends DragDropManager<PanDraggable, Droppable> {
|
|
98
|
+
options: DndContextProps = {}
|
|
99
|
+
readonly draggableNodes = new Map<DndId, any>()
|
|
100
|
+
readonly droppableNodes = new Map<DndId, any>()
|
|
101
|
+
readonly scrollShift$ = signal({ x: 0, y: 0 })
|
|
102
|
+
private listeners = new Set<() => void>()
|
|
103
|
+
private snapshot: DndSnapshot = {
|
|
104
|
+
active: null,
|
|
105
|
+
over: null,
|
|
106
|
+
transform: { x: 0, y: 0 },
|
|
107
|
+
isDragging: false,
|
|
108
|
+
}
|
|
109
|
+
private dispose: () => void
|
|
110
|
+
private scrollTimer: ReturnType<typeof setInterval> | undefined
|
|
111
|
+
private destroyed = false
|
|
112
|
+
constructor() {
|
|
113
|
+
super({ sensors: [PanSensor] })
|
|
114
|
+
this.dispose = effect(() => {
|
|
115
|
+
const operation = this.dragOperation
|
|
116
|
+
const active = operation.source
|
|
117
|
+
const over = operation.target
|
|
118
|
+
const transform = operation.transform
|
|
119
|
+
const isDragging = operation.status.dragging
|
|
120
|
+
this.snapshot = {
|
|
121
|
+
active: active && !operation.status.idle ? { id: active.id, data: active.data } : null,
|
|
122
|
+
over: over && !operation.status.idle ? { id: over.id, data: over.data } : null,
|
|
123
|
+
transform: {
|
|
124
|
+
x: operation.status.idle ? 0 : transform.x + this.scrollShift$.value.x,
|
|
125
|
+
y: operation.status.idle ? 0 : transform.y + this.scrollShift$.value.y,
|
|
126
|
+
},
|
|
127
|
+
isDragging,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
untracked(() => {
|
|
131
|
+
for (const listener of this.listeners) {
|
|
132
|
+
listener()
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const event = (canceled = false): DragEvent => {
|
|
138
|
+
const operation = this.dragOperation
|
|
139
|
+
return {
|
|
140
|
+
active: operation.source ? { id: operation.source.id, data: operation.source.data } : null,
|
|
141
|
+
over: operation.target ? { id: operation.target.id, data: operation.target.data } : null,
|
|
142
|
+
transform: { ...operation.transform },
|
|
143
|
+
isDragging: operation.status.dragging,
|
|
144
|
+
canceled,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.monitor.addEventListener('dragstart', () => this.options.onDragStart?.(event()))
|
|
149
|
+
this.monitor.addEventListener('dragover', () => this.options.onDragOver?.(event()))
|
|
150
|
+
// dragmove fires before position is committed, so deliver the committed snapshot.
|
|
151
|
+
this.monitor.addEventListener('dragmove', () =>
|
|
152
|
+
queueMicrotask(() =>
|
|
153
|
+
queueMicrotask(() => {
|
|
154
|
+
if (!this.destroyed && this.dragOperation.status.dragging) {
|
|
155
|
+
this.options.onDragMove?.(event())
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
this.monitor.addEventListener('dragend', ({ canceled }) => {
|
|
162
|
+
this.stopAutoScroll()
|
|
163
|
+
if (canceled) {
|
|
164
|
+
this.options.onDragCancel?.(event(true))
|
|
165
|
+
} else {
|
|
166
|
+
this.options.onDragEnd?.(event())
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
getSnapshot = (): DndSnapshot => this.snapshot
|
|
171
|
+
subscribe = (listener: () => void) => {
|
|
172
|
+
this.listeners.add(listener)
|
|
173
|
+
return () => {
|
|
174
|
+
this.listeners.delete(listener)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
registerDraggable(options: DraggableOptions) {
|
|
178
|
+
if (this.registry.draggables.has(options.id)) {
|
|
179
|
+
throw new Error(`Duplicate draggable id: ${options.id}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const source: PanDraggable = new PanDraggable(
|
|
183
|
+
{
|
|
184
|
+
...options,
|
|
185
|
+
register: false,
|
|
186
|
+
effects: () => [() => this.registry.sensors.get(PanSensor)!.bind(source)],
|
|
187
|
+
},
|
|
188
|
+
this,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
source.register()
|
|
192
|
+
return source
|
|
193
|
+
}
|
|
194
|
+
registerDroppable(options: DroppableOptions) {
|
|
195
|
+
if (this.registry.droppables.has(options.id)) {
|
|
196
|
+
throw new Error(`Duplicate droppable id: ${options.id}`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const target = new Droppable(
|
|
200
|
+
{
|
|
201
|
+
...options,
|
|
202
|
+
register: false,
|
|
203
|
+
accept: options.accept
|
|
204
|
+
? (source) => options.accept!({ id: source.id, data: source.data })
|
|
205
|
+
: undefined,
|
|
206
|
+
collisionDetector: (input) =>
|
|
207
|
+
input.dragOperation.source?.id === input.droppable.id
|
|
208
|
+
? null
|
|
209
|
+
: (this.options.collisionDetection ?? defaultCollisionDetection)(input),
|
|
210
|
+
},
|
|
211
|
+
this,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
target.register()
|
|
215
|
+
return target
|
|
216
|
+
}
|
|
217
|
+
refresh() {
|
|
218
|
+
batch(() => {
|
|
219
|
+
for (const droppable of this.registry.droppables) {
|
|
220
|
+
droppable.shape = measure(this.droppableNodes.get(droppable.id)) ?? undefined
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
this.collisionObserver.forceUpdate()
|
|
225
|
+
}
|
|
226
|
+
cancel() {
|
|
227
|
+
this.stopAutoScroll()
|
|
228
|
+
this.actions.stop({ canceled: true })
|
|
229
|
+
}
|
|
230
|
+
startAutoScroll() {
|
|
231
|
+
if (this.scrollTimer || !this.options.autoScroll) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.scrollTimer = setInterval(() => {
|
|
236
|
+
const scroll = this.options.autoScroll
|
|
237
|
+
if (!scroll || !this.dragOperation.status.dragging) {
|
|
238
|
+
this.stopAutoScroll()
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const bounds = scroll.bounds()
|
|
243
|
+
if (!bounds) {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const horizontal = scroll.axis === 'x'
|
|
248
|
+
const start = horizontal ? bounds.x : bounds.y
|
|
249
|
+
const size = horizontal ? bounds.width : bounds.height
|
|
250
|
+
const point = horizontal
|
|
251
|
+
? this.dragOperation.position.current.x
|
|
252
|
+
: this.dragOperation.position.current.y
|
|
253
|
+
|
|
254
|
+
const threshold = Math.min(scroll.threshold ?? 40, size / 2)
|
|
255
|
+
if (threshold <= 0 || point < start || point > start + size) {
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const strength =
|
|
260
|
+
point < start + threshold
|
|
261
|
+
? -(1 - (point - start) / threshold)
|
|
262
|
+
: point > start + size - threshold
|
|
263
|
+
? 1 - (start + size - point) / threshold
|
|
264
|
+
: 0
|
|
265
|
+
|
|
266
|
+
const next = Math.max(
|
|
267
|
+
0,
|
|
268
|
+
Math.min(scroll.maxOffset(), scroll.offsetRef.current + strength * (scroll.speed ?? 12)),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if (next !== scroll.offsetRef.current) {
|
|
272
|
+
const before = scroll.offsetRef.current
|
|
273
|
+
scroll.scrollTo(next)
|
|
274
|
+
const delta = scroll.offsetRef.current - before
|
|
275
|
+
const shift = this.scrollShift$.peek()
|
|
276
|
+
this.scrollShift$.value = {
|
|
277
|
+
x: shift.x + (horizontal ? delta : 0),
|
|
278
|
+
y: shift.y + (horizontal ? 0 : delta),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.refresh()
|
|
282
|
+
}
|
|
283
|
+
}, 16)
|
|
284
|
+
}
|
|
285
|
+
private stopAutoScroll() {
|
|
286
|
+
if (this.scrollTimer) {
|
|
287
|
+
clearInterval(this.scrollTimer)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.scrollTimer = undefined
|
|
291
|
+
}
|
|
292
|
+
disposeController() {
|
|
293
|
+
this.destroyed = true
|
|
294
|
+
this.cancel()
|
|
295
|
+
this.dispose()
|
|
296
|
+
this.listeners.clear()
|
|
297
|
+
this.draggableNodes.clear()
|
|
298
|
+
this.droppableNodes.clear()
|
|
299
|
+
this.destroy()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { it, expect, vi } from 'vitest'
|
|
2
|
+
import { measure, dragStyle } from './geometry.macos'
|
|
3
|
+
|
|
4
|
+
it('uses converted window bounds and normalizes unflipped coordinates', () => {
|
|
5
|
+
const content = {
|
|
6
|
+
bounds: { origin: { x: 5, y: 10 }, size: { width: 500, height: 400 } },
|
|
7
|
+
isFlipped: false,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const convertRectToView = vi.fn(() => ({
|
|
11
|
+
origin: { x: 25, y: 310 },
|
|
12
|
+
size: { width: 100, height: 40 },
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
const view = { window: { contentView: content }, bounds: {}, convertRectToView }
|
|
16
|
+
const rect = measure(view)!
|
|
17
|
+
expect(rect.boundingRectangle).toMatchObject({ left: 20, top: 60, width: 100, height: 40 })
|
|
18
|
+
expect(convertRectToView).toHaveBeenCalledWith(view.bounds, content)
|
|
19
|
+
content.isFlipped = true
|
|
20
|
+
expect(measure(view)!.boundingRectangle.top).toBe(300)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('ignores detached and zero-size views and exposes reversible drag styles', () => {
|
|
24
|
+
expect(measure({})).toBeNull()
|
|
25
|
+
expect(
|
|
26
|
+
measure({
|
|
27
|
+
window: { contentView: {} },
|
|
28
|
+
convertRectToView: () => ({ size: { width: 0, height: 10 } }),
|
|
29
|
+
}),
|
|
30
|
+
).toBeNull()
|
|
31
|
+
|
|
32
|
+
expect(dragStyle(12, 30)).toEqual({ translateX: 12, translateY: 30, zIndex: 1 })
|
|
33
|
+
expect(dragStyle(0, 0)).toEqual({ translateX: 0, translateY: 0, zIndex: 0 })
|
|
34
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Rectangle } from '@dnd-kit/geometry'
|
|
2
|
+
|
|
3
|
+
/** AppKit points in top-left window-content coordinates, independent of parent and scroll offsets. */
|
|
4
|
+
export function measure(view: any): Rectangle | null {
|
|
5
|
+
const content = view?.window?.contentView
|
|
6
|
+
if (!content || !view?.convertRectToView) {
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const frame = view.convertRectToView(view.bounds, content)
|
|
11
|
+
if (frame.size.width <= 0 || frame.size.height <= 0) {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const bounds = content.bounds
|
|
16
|
+
const flipped = typeof content.isFlipped === 'function' ? content.isFlipped() : content.isFlipped
|
|
17
|
+
const x = Number(frame.origin.x) - Number(bounds.origin.x)
|
|
18
|
+
const y = flipped
|
|
19
|
+
? Number(frame.origin.y) - Number(bounds.origin.y)
|
|
20
|
+
: Number(bounds.origin.y) +
|
|
21
|
+
Number(bounds.size.height) -
|
|
22
|
+
Number(frame.origin.y) -
|
|
23
|
+
Number(frame.size.height)
|
|
24
|
+
|
|
25
|
+
return frame.size.width > 0 && frame.size.height > 0
|
|
26
|
+
? new Rectangle(x, y, Number(frame.size.width), Number(frame.size.height))
|
|
27
|
+
: null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function dragStyle(x: number, y: number): Record<string, number | string> {
|
|
31
|
+
return { translateX: x, translateY: y, zIndex: x || y ? 1 : 0 }
|
|
32
|
+
}
|
package/src/geometry.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Rectangle } from '@dnd-kit/geometry'
|
|
2
|
+
|
|
3
|
+
/** NativeScript screen coordinates and dimensions are in DIPs. */
|
|
4
|
+
export function measure(element: any): Rectangle | null {
|
|
5
|
+
if (!element || element.isLoaded === false) {
|
|
6
|
+
return null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const point = element.getLocationOnScreen?.()
|
|
10
|
+
const size = element.getActualSize?.()
|
|
11
|
+
return point && size && size.width > 0 && size.height > 0
|
|
12
|
+
? new Rectangle(point.x, point.y, size.width, size.height)
|
|
13
|
+
: null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function dragStyle(x: number, y: number): Record<string, number | string> {
|
|
17
|
+
return { translateX: x, translateY: y, zIndex: x || y ? 1 : 0 }
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Rectangle } from '@dnd-kit/geometry'
|
|
2
|
+
|
|
3
|
+
export function measure(element: any): Rectangle | null {
|
|
4
|
+
if (!element?.isConnected) {
|
|
5
|
+
return null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const rect = element.getBoundingClientRect()
|
|
9
|
+
return rect.width > 0 && rect.height > 0 ? Rectangle.from(rect) : null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function dragStyle(x: number, y: number): Record<string, number | string> {
|
|
13
|
+
return { transform: `translate(${x}px, ${y}px)`, touchAction: 'none', zIndex: x || y ? 1 : 0 }
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import './abort.macos'
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
DndContext,
|
|
5
|
+
useDndContext,
|
|
6
|
+
useDraggable,
|
|
7
|
+
useDroppable,
|
|
8
|
+
SortableContext,
|
|
9
|
+
useSortable,
|
|
10
|
+
SortableList,
|
|
11
|
+
} from './components'
|
|
12
|
+
|
|
13
|
+
export { arrayMove } from './sort'
|
|
14
|
+
export {
|
|
15
|
+
closestCenter,
|
|
16
|
+
closestCorners,
|
|
17
|
+
pointerIntersection,
|
|
18
|
+
shapeIntersection,
|
|
19
|
+
defaultCollisionDetection,
|
|
20
|
+
} from '@dnd-kit/collision'
|
|
21
|
+
|
|
22
|
+
export type { CollisionDetector } from '@dnd-kit/abstract'
|
|
23
|
+
export type * from './props'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DndContext,
|
|
3
|
+
useDndContext,
|
|
4
|
+
useDraggable,
|
|
5
|
+
useDroppable,
|
|
6
|
+
SortableContext,
|
|
7
|
+
useSortable,
|
|
8
|
+
SortableList,
|
|
9
|
+
} from './components'
|
|
10
|
+
|
|
11
|
+
export { arrayMove } from './sort'
|
|
12
|
+
export {
|
|
13
|
+
closestCenter,
|
|
14
|
+
closestCorners,
|
|
15
|
+
pointerIntersection,
|
|
16
|
+
shapeIntersection,
|
|
17
|
+
defaultCollisionDetection,
|
|
18
|
+
} from '@dnd-kit/collision'
|
|
19
|
+
|
|
20
|
+
export type { CollisionDetector } from '@dnd-kit/abstract'
|
|
21
|
+
export type * from './props'
|
package/src/index.web.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DndContext,
|
|
3
|
+
useDndContext,
|
|
4
|
+
useDraggable,
|
|
5
|
+
useDroppable,
|
|
6
|
+
SortableContext,
|
|
7
|
+
useSortable,
|
|
8
|
+
SortableList,
|
|
9
|
+
} from './components'
|
|
10
|
+
|
|
11
|
+
export { arrayMove } from './sort'
|
|
12
|
+
export {
|
|
13
|
+
closestCenter,
|
|
14
|
+
closestCorners,
|
|
15
|
+
pointerIntersection,
|
|
16
|
+
shapeIntersection,
|
|
17
|
+
defaultCollisionDetection,
|
|
18
|
+
} from '@dnd-kit/collision'
|
|
19
|
+
|
|
20
|
+
export type { CollisionDetector } from '@dnd-kit/abstract'
|
|
21
|
+
export type * from './props'
|
package/src/props.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { CollisionDetector, Data, UniqueIdentifier } from '@dnd-kit/abstract'
|
|
2
|
+
import type { PanEvent, ViewProps } from '@octane-xplat/ui'
|
|
3
|
+
|
|
4
|
+
export type DndId = UniqueIdentifier
|
|
5
|
+
export interface DndItem {
|
|
6
|
+
id: DndId
|
|
7
|
+
data: Data
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DndSnapshot {
|
|
11
|
+
active: DndItem | null
|
|
12
|
+
over: DndItem | null
|
|
13
|
+
transform: { x: number; y: number }
|
|
14
|
+
isDragging: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DragEvent extends DndSnapshot {
|
|
18
|
+
canceled: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Supply current viewport bounds and offsets; scrollTo must update offsetRef synchronously. */
|
|
22
|
+
export interface AutoScroll {
|
|
23
|
+
bounds(): { x: number; y: number; width: number; height: number } | null
|
|
24
|
+
offsetRef: { current: number }
|
|
25
|
+
maxOffset(): number
|
|
26
|
+
scrollTo(offset: number): void
|
|
27
|
+
axis?: 'x' | 'y'
|
|
28
|
+
threshold?: number
|
|
29
|
+
speed?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DndContextProps {
|
|
33
|
+
children?: ViewProps['children']
|
|
34
|
+
collisionDetection?: CollisionDetector
|
|
35
|
+
autoScroll?: AutoScroll
|
|
36
|
+
onDragStart?: (event: DragEvent) => void
|
|
37
|
+
onDragMove?: (event: DragEvent) => void
|
|
38
|
+
onDragOver?: (event: DragEvent) => void
|
|
39
|
+
onDragEnd?: (event: DragEvent) => void
|
|
40
|
+
onDragCancel?: (event: DragEvent) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DraggableOptions {
|
|
44
|
+
id: DndId
|
|
45
|
+
data?: Data
|
|
46
|
+
disabled?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DroppableOptions extends DraggableOptions {
|
|
50
|
+
accept?: (source: DndItem) => boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DraggableResult {
|
|
54
|
+
ref(element: any): void
|
|
55
|
+
onPan(event: PanEvent): void
|
|
56
|
+
isDragging: boolean
|
|
57
|
+
transform: { x: number; y: number }
|
|
58
|
+
/** Spread into View with ref and onPan. */
|
|
59
|
+
style: NonNullable<ViewProps['style']>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DroppableResult {
|
|
63
|
+
ref(element: any): void
|
|
64
|
+
isOver: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SortableResult extends DraggableResult {
|
|
68
|
+
isOver: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SortableContextProps {
|
|
72
|
+
items: readonly DndId[]
|
|
73
|
+
children?: ViewProps['children']
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SortableListProps<T extends DndId = DndId> extends Omit<ViewProps, 'children'> {
|
|
77
|
+
items: readonly T[]
|
|
78
|
+
renderItem(id: T, index: number): ViewProps['children']
|
|
79
|
+
/** Called after a successful drop within this list; caller owns the item order. */
|
|
80
|
+
onReorder(items: T[], event: DragEvent): void
|
|
81
|
+
disabled?: boolean
|
|
82
|
+
dnd?: Omit<DndContextProps, 'children'>
|
|
83
|
+
}
|
package/src/sort.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Return a new order. Invalid indices leave the order unchanged. */
|
|
2
|
+
export function arrayMove<T>(items: readonly T[], from: number, to: number): T[] {
|
|
3
|
+
const result = [...items]
|
|
4
|
+
if (
|
|
5
|
+
!Number.isInteger(from) ||
|
|
6
|
+
!Number.isInteger(to) ||
|
|
7
|
+
from < 0 ||
|
|
8
|
+
to < 0 ||
|
|
9
|
+
from >= items.length ||
|
|
10
|
+
to >= items.length ||
|
|
11
|
+
from === to
|
|
12
|
+
) {
|
|
13
|
+
return result
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [item] = result.splice(from, 1)
|
|
17
|
+
result.splice(to, 0, item!)
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DndId, DndContextProps, DndSnapshot, DraggableOptions, DraggableResult, DroppableOptions, DroppableResult, SortableContextProps, SortableResult, SortableListProps } from './props.js';
|
|
2
|
+
/** Scope registrations to one renderer root. Nested roots need their own DndContext. */
|
|
3
|
+
export declare function DndContext(props: DndContextProps): import("octane/jsx-runtime").JSX.Element;
|
|
4
|
+
/** Subscribes independently so retained native children observe drag state. */
|
|
5
|
+
export declare function useDndContext(): DndSnapshot;
|
|
6
|
+
export declare function useDraggable(options: DraggableOptions): DraggableResult;
|
|
7
|
+
export declare function useDroppable(options: DroppableOptions): DroppableResult;
|
|
8
|
+
/** Declares list membership for useSortable; it does not own item order. */
|
|
9
|
+
export declare function SortableContext(props: SortableContextProps): import("octane/jsx-runtime").JSX.Element;
|
|
10
|
+
/** Bind and spread onPan/style onto one View. Order changes only when the owner commits a drop. */
|
|
11
|
+
export declare function useSortable(options: DraggableOptions): SortableResult;
|
|
12
|
+
/** A controlled sortable list. Provide stable unique ids and save the order in onReorder. */
|
|
13
|
+
export declare function SortableList<T extends DndId>(props: SortableListProps<T>): import("octane/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DragDropManager, Draggable, Droppable } from '@dnd-kit/abstract';
|
|
2
|
+
import type { PanEvent } from '@octane-xplat/ui';
|
|
3
|
+
import type { DndContextProps, DndSnapshot, DraggableOptions, DroppableOptions } from './props.js';
|
|
4
|
+
declare class PanDraggable extends Draggable {
|
|
5
|
+
onPan?: (event: PanEvent) => void;
|
|
6
|
+
}
|
|
7
|
+
/** One controller per DndContext; no global registry or cross-root context assumptions. */
|
|
8
|
+
export declare class DndController extends DragDropManager<PanDraggable, Droppable> {
|
|
9
|
+
options: DndContextProps;
|
|
10
|
+
readonly draggableNodes: Map<import("@dnd-kit/abstract").UniqueIdentifier, any>;
|
|
11
|
+
readonly droppableNodes: Map<import("@dnd-kit/abstract").UniqueIdentifier, any>;
|
|
12
|
+
readonly scrollShift$: import("@dnd-kit/state").Signal<{
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
}>;
|
|
16
|
+
private listeners;
|
|
17
|
+
private snapshot;
|
|
18
|
+
private dispose;
|
|
19
|
+
private scrollTimer;
|
|
20
|
+
private destroyed;
|
|
21
|
+
constructor();
|
|
22
|
+
getSnapshot: () => DndSnapshot;
|
|
23
|
+
subscribe: (listener: () => void) => () => void;
|
|
24
|
+
registerDraggable(options: DraggableOptions): PanDraggable;
|
|
25
|
+
registerDroppable(options: DroppableOptions): Droppable<import("@dnd-kit/abstract").Data, this>;
|
|
26
|
+
refresh(): void;
|
|
27
|
+
cancel(): void;
|
|
28
|
+
startAutoScroll(): void;
|
|
29
|
+
private stopAutoScroll;
|
|
30
|
+
disposeController(): void;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DndContext, useDndContext, useDraggable, useDroppable, SortableContext, useSortable, SortableList, } from './components.js';
|
|
2
|
+
export { arrayMove } from './sort.js';
|
|
3
|
+
export { closestCenter, closestCorners, pointerIntersection, shapeIntersection, defaultCollisionDetection, } from '@dnd-kit/collision';
|
|
4
|
+
export type { CollisionDetector } from '@dnd-kit/abstract';
|
|
5
|
+
export type * from './props.js';
|