@octane-xplat/dnd-kit 0.0.0-stage → 0.10.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.
@@ -0,0 +1,236 @@
1
+ import { measure } from "./geometry.web.js";
2
+ import { DragDropManager, Draggable, Droppable, Sensor } from "@dnd-kit/abstract";
3
+ import { defaultCollisionDetection } from "@dnd-kit/collision";
4
+ import { batch, effect, signal, untracked } from "@dnd-kit/state";
5
+ //#region src/controller.ts
6
+ var PanDraggable = class extends Draggable {
7
+ onPan;
8
+ };
9
+ /** Xplat View supplies normalized pan input; the abstract core owns the drag lifecycle. */
10
+ var PanSensor = class extends Sensor {
11
+ bind(input) {
12
+ const source = input;
13
+ let origin = null;
14
+ let bound = true;
15
+ let base = {
16
+ x: 0,
17
+ y: 0
18
+ };
19
+ let queue = Promise.resolve();
20
+ const handle = async (event) => {
21
+ const manager = this.manager;
22
+ if (!bound) return;
23
+ if (event.state === "began") {
24
+ if (source.disabled || !manager.dragOperation.status.idle) return;
25
+ origin = measure(manager.draggableNodes.get(source.id));
26
+ if (!origin) return;
27
+ base = {
28
+ x: event.dx,
29
+ y: event.dy
30
+ };
31
+ manager.scrollShift$.value = {
32
+ x: 0,
33
+ y: 0
34
+ };
35
+ manager.actions.start({
36
+ source,
37
+ coordinates: origin.center
38
+ });
39
+ manager.dragOperation.shape = origin;
40
+ manager.refresh();
41
+ await manager.renderer.rendering;
42
+ return;
43
+ }
44
+ if (!origin || manager.dragOperation.source?.id !== source.id || !manager.dragOperation.status.dragging) return;
45
+ const x = event.dx - base.x;
46
+ const y = event.dy - base.y;
47
+ manager.actions.move({ to: {
48
+ x: origin.center.x + x,
49
+ y: origin.center.y + y
50
+ } });
51
+ await Promise.resolve();
52
+ manager.dragOperation.shape = origin.translate(x, y);
53
+ manager.refresh();
54
+ if (event.state === "ended" || event.state === "cancelled") {
55
+ manager.actions.stop({ canceled: event.state === "cancelled" });
56
+ origin = null;
57
+ } else manager.startAutoScroll();
58
+ };
59
+ source.onPan = (event) => {
60
+ queue = queue.then(() => handle(event)).catch((error) => {
61
+ origin = null;
62
+ this.manager.cancel();
63
+ console.error("[dnd-kit] pan failed", error);
64
+ });
65
+ };
66
+ return () => {
67
+ bound = false;
68
+ source.onPan = void 0;
69
+ origin = null;
70
+ };
71
+ }
72
+ };
73
+ /** One controller per DndContext; no global registry or cross-root context assumptions. */
74
+ var DndController = class extends DragDropManager {
75
+ options = {};
76
+ draggableNodes = /* @__PURE__ */ new Map();
77
+ droppableNodes = /* @__PURE__ */ new Map();
78
+ scrollShift$ = signal({
79
+ x: 0,
80
+ y: 0
81
+ });
82
+ listeners = /* @__PURE__ */ new Set();
83
+ snapshot = {
84
+ active: null,
85
+ over: null,
86
+ transform: {
87
+ x: 0,
88
+ y: 0
89
+ },
90
+ isDragging: false
91
+ };
92
+ dispose;
93
+ scrollTimer;
94
+ destroyed = false;
95
+ constructor() {
96
+ super({ sensors: [PanSensor] });
97
+ this.dispose = effect(() => {
98
+ const operation = this.dragOperation;
99
+ const active = operation.source;
100
+ const over = operation.target;
101
+ const transform = operation.transform;
102
+ const isDragging = operation.status.dragging;
103
+ this.snapshot = {
104
+ active: active && !operation.status.idle ? {
105
+ id: active.id,
106
+ data: active.data
107
+ } : null,
108
+ over: over && !operation.status.idle ? {
109
+ id: over.id,
110
+ data: over.data
111
+ } : null,
112
+ transform: {
113
+ x: operation.status.idle ? 0 : transform.x + this.scrollShift$.value.x,
114
+ y: operation.status.idle ? 0 : transform.y + this.scrollShift$.value.y
115
+ },
116
+ isDragging
117
+ };
118
+ untracked(() => {
119
+ for (const listener of this.listeners) listener();
120
+ });
121
+ });
122
+ const event = (canceled = false) => {
123
+ const operation = this.dragOperation;
124
+ return {
125
+ active: operation.source ? {
126
+ id: operation.source.id,
127
+ data: operation.source.data
128
+ } : null,
129
+ over: operation.target ? {
130
+ id: operation.target.id,
131
+ data: operation.target.data
132
+ } : null,
133
+ transform: { ...operation.transform },
134
+ isDragging: operation.status.dragging,
135
+ canceled
136
+ };
137
+ };
138
+ this.monitor.addEventListener("dragstart", () => this.options.onDragStart?.(event()));
139
+ this.monitor.addEventListener("dragover", () => this.options.onDragOver?.(event()));
140
+ this.monitor.addEventListener("dragmove", () => queueMicrotask(() => queueMicrotask(() => {
141
+ if (!this.destroyed && this.dragOperation.status.dragging) this.options.onDragMove?.(event());
142
+ })));
143
+ this.monitor.addEventListener("dragend", ({ canceled }) => {
144
+ this.stopAutoScroll();
145
+ if (canceled) this.options.onDragCancel?.(event(true));
146
+ else this.options.onDragEnd?.(event());
147
+ });
148
+ }
149
+ getSnapshot = () => this.snapshot;
150
+ subscribe = (listener) => {
151
+ this.listeners.add(listener);
152
+ return () => {
153
+ this.listeners.delete(listener);
154
+ };
155
+ };
156
+ registerDraggable(options) {
157
+ if (this.registry.draggables.has(options.id)) throw new Error(`Duplicate draggable id: ${options.id}`);
158
+ const source = new PanDraggable({
159
+ ...options,
160
+ register: false,
161
+ effects: () => [() => this.registry.sensors.get(PanSensor).bind(source)]
162
+ }, this);
163
+ source.register();
164
+ return source;
165
+ }
166
+ registerDroppable(options) {
167
+ if (this.registry.droppables.has(options.id)) throw new Error(`Duplicate droppable id: ${options.id}`);
168
+ const target = new Droppable({
169
+ ...options,
170
+ register: false,
171
+ accept: options.accept ? (source) => options.accept({
172
+ id: source.id,
173
+ data: source.data
174
+ }) : void 0,
175
+ collisionDetector: (input) => input.dragOperation.source?.id === input.droppable.id ? null : (this.options.collisionDetection ?? defaultCollisionDetection)(input)
176
+ }, this);
177
+ target.register();
178
+ return target;
179
+ }
180
+ refresh() {
181
+ batch(() => {
182
+ for (const droppable of this.registry.droppables) droppable.shape = measure(this.droppableNodes.get(droppable.id)) ?? void 0;
183
+ });
184
+ this.collisionObserver.forceUpdate();
185
+ }
186
+ cancel() {
187
+ this.stopAutoScroll();
188
+ this.actions.stop({ canceled: true });
189
+ }
190
+ startAutoScroll() {
191
+ if (this.scrollTimer || !this.options.autoScroll) return;
192
+ this.scrollTimer = setInterval(() => {
193
+ const scroll = this.options.autoScroll;
194
+ if (!scroll || !this.dragOperation.status.dragging) {
195
+ this.stopAutoScroll();
196
+ return;
197
+ }
198
+ const bounds = scroll.bounds();
199
+ if (!bounds) return;
200
+ const horizontal = scroll.axis === "x";
201
+ const start = horizontal ? bounds.x : bounds.y;
202
+ const size = horizontal ? bounds.width : bounds.height;
203
+ const point = horizontal ? this.dragOperation.position.current.x : this.dragOperation.position.current.y;
204
+ const threshold = Math.min(scroll.threshold ?? 40, size / 2);
205
+ if (threshold <= 0 || point < start || point > start + size) return;
206
+ const strength = point < start + threshold ? -(1 - (point - start) / threshold) : point > start + size - threshold ? 1 - (start + size - point) / threshold : 0;
207
+ const next = Math.max(0, Math.min(scroll.maxOffset(), scroll.offsetRef.current + strength * (scroll.speed ?? 12)));
208
+ if (next !== scroll.offsetRef.current) {
209
+ const before = scroll.offsetRef.current;
210
+ scroll.scrollTo(next);
211
+ const delta = scroll.offsetRef.current - before;
212
+ const shift = this.scrollShift$.peek();
213
+ this.scrollShift$.value = {
214
+ x: shift.x + (horizontal ? delta : 0),
215
+ y: shift.y + (horizontal ? 0 : delta)
216
+ };
217
+ this.refresh();
218
+ }
219
+ }, 16);
220
+ }
221
+ stopAutoScroll() {
222
+ if (this.scrollTimer) clearInterval(this.scrollTimer);
223
+ this.scrollTimer = void 0;
224
+ }
225
+ disposeController() {
226
+ this.destroyed = true;
227
+ this.cancel();
228
+ this.dispose();
229
+ this.listeners.clear();
230
+ this.draggableNodes.clear();
231
+ this.droppableNodes.clear();
232
+ this.destroy();
233
+ }
234
+ };
235
+ //#endregion
236
+ export { DndController };
@@ -0,0 +1,16 @@
1
+ import { Rectangle } from "@dnd-kit/geometry";
2
+ //#region src/geometry.web.ts
3
+ function measure(element) {
4
+ if (!element?.isConnected) return null;
5
+ const rect = element.getBoundingClientRect();
6
+ return rect.width > 0 && rect.height > 0 ? Rectangle.from(rect) : null;
7
+ }
8
+ function dragStyle(x, y) {
9
+ return {
10
+ transform: `translate(${x}px, ${y}px)`,
11
+ touchAction: "none",
12
+ zIndex: x || y ? 1 : 0
13
+ };
14
+ }
15
+ //#endregion
16
+ export { dragStyle, measure };
@@ -0,0 +1,4 @@
1
+ import { arrayMove } from "./sort.js";
2
+ import { DndContext, SortableContext, SortableList, useDndContext, useDraggable, useDroppable, useSortable } from "./components.js";
3
+ import { closestCenter, closestCorners, defaultCollisionDetection, pointerIntersection, shapeIntersection } from "@dnd-kit/collision";
4
+ export { DndContext, SortableContext, SortableList, arrayMove, closestCenter, closestCorners, defaultCollisionDetection, pointerIntersection, shapeIntersection, useDndContext, useDraggable, useDroppable, useSortable };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "35a78e2b-b2ce-4aa0-8968-708058c15fed",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,11 @@
1
+ //#region src/sort.ts
2
+ /** Return a new order. Invalid indices leave the order unchanged. */
3
+ function arrayMove(items, from, to) {
4
+ const result = [...items];
5
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < 0 || from >= items.length || to >= items.length || from === to) return result;
6
+ const [item] = result.splice(from, 1);
7
+ result.splice(to, 0, item);
8
+ return result;
9
+ }
10
+ //#endregion
11
+ export { arrayMove };
@@ -0,0 +1,128 @@
1
+ /** @jsxImportSource @octane-xplat/macos-renderer */
2
+ import { useCallback, useRef, useState } from 'octane'
3
+ import { Text, View, useMeasure } from '@octane-xplat/ui'
4
+ import { SortableList } from '../src/index'
5
+
6
+ /** AppKit scroll ownership stays explicit; all geometry and offsets use top-down points. */
7
+ export default function SortableExample() {
8
+ const [items, setItems] = useState(Array.from({ length: 20 }, (_, index) => `task-${index + 1}`))
9
+ const scrollRef = useRef<any>(null)
10
+ const offsetRef = useRef(0)
11
+ const initialized = useRef(false)
12
+ const measure = useMeasure()
13
+ const bindScroll = useCallback(
14
+ (view: any) => {
15
+ scrollRef.current = view
16
+ measure.ref(view?.contentView ?? null)
17
+ },
18
+ [measure.ref],
19
+ )
20
+
21
+ const top = () => {
22
+ const scroll = scrollRef.current
23
+ const clip = scroll?.contentView
24
+ const content = scroll?.documentView
25
+ if (!clip || !content) {
26
+ return 0
27
+ }
28
+
29
+ const flipped =
30
+ typeof content.isFlipped === 'function' ? content.isFlipped() : content.isFlipped
31
+
32
+ return flipped
33
+ ? Number(clip.bounds.origin.y)
34
+ : Number(content.frame.size.height) -
35
+ Number(clip.bounds.size.height) -
36
+ Number(clip.bounds.origin.y)
37
+ }
38
+
39
+ return (
40
+ <View>
41
+ <Text id="dnd-order">{items.join(',')}</Text>
42
+ <scrollview
43
+ id="dnd-scroll"
44
+ style={{ height: 240 }}
45
+ ref={bindScroll}
46
+ onScroll={() => {
47
+ offsetRef.current = top()
48
+ }}
49
+ >
50
+ <flexboxlayout
51
+ flexDirection="column"
52
+ onLayoutChanged={() => {
53
+ const scroll = scrollRef.current
54
+ if (
55
+ initialized.current ||
56
+ !scroll?.window ||
57
+ scroll.documentView.frame.size.height <= scroll.contentView.bounds.size.height
58
+ ) {
59
+ return
60
+ }
61
+
62
+ initialized.current = true
63
+ const content = scroll.documentView
64
+ const flipped =
65
+ typeof content.isFlipped === 'function' ? content.isFlipped() : content.isFlipped
66
+
67
+ scroll.contentView.scrollToPoint({
68
+ x: 0,
69
+ y: flipped
70
+ ? 0
71
+ : Math.max(0, content.frame.size.height - scroll.contentView.bounds.size.height),
72
+ })
73
+
74
+ scroll.reflectScrolledClipView(scroll.contentView)
75
+ measure.ref(scroll.contentView)
76
+ offsetRef.current = top()
77
+ }}
78
+ >
79
+ <SortableList
80
+ items={items}
81
+ onReorder={setItems}
82
+ dnd={{
83
+ autoScroll: {
84
+ bounds: () => measure.bounds,
85
+ offsetRef,
86
+ maxOffset: () => {
87
+ const scroll = scrollRef.current
88
+ return Math.max(
89
+ 0,
90
+ Number(scroll?.documentView?.frame?.size?.height ?? 0) -
91
+ Number(scroll?.contentView?.bounds?.size?.height ?? 0),
92
+ )
93
+ },
94
+ scrollTo: (offset) => {
95
+ const scroll = scrollRef.current
96
+ const content = scroll.documentView
97
+ const clip = scroll.contentView
98
+ const flipped =
99
+ typeof content.isFlipped === 'function'
100
+ ? content.isFlipped()
101
+ : content.isFlipped
102
+
103
+ const y = flipped
104
+ ? offset
105
+ : Math.max(
106
+ 0,
107
+ Number(content.frame.size.height) -
108
+ Number(clip.bounds.size.height) -
109
+ offset,
110
+ )
111
+
112
+ clip.scrollToPoint({ x: Number(clip.bounds.origin.x), y })
113
+ scroll.reflectScrolledClipView(clip)
114
+ offsetRef.current = top()
115
+ },
116
+ },
117
+ }}
118
+ renderItem={(id) => (
119
+ <View id={`dnd-${id}`} style={{ height: 64, padding: 12 }}>
120
+ <Text>{id}</Text>
121
+ </View>
122
+ )}
123
+ />
124
+ </flexboxlayout>
125
+ </scrollview>
126
+ </View>
127
+ )
128
+ }
@@ -0,0 +1,22 @@
1
+ import { useState } from 'octane'
2
+ import { Text, View } from '@octane-xplat/ui'
3
+ import { SortableList } from '../src/index'
4
+
5
+ /** Controlled order: drag a row onto another row to commit a reorder. */
6
+ export default function SortableExample() {
7
+ const [items, setItems] = useState(['alpha', 'beta', 'gamma'])
8
+ return (
9
+ <View>
10
+ <Text id="dnd-order">{items.join(',')}</Text>
11
+ <SortableList
12
+ items={items}
13
+ onReorder={setItems}
14
+ renderItem={(id) => (
15
+ <View id={`dnd-${id}`} style={{ height: 64, padding: 12 }}>
16
+ <Text>{String(id)}</Text>
17
+ </View>
18
+ )}
19
+ />
20
+ </View>
21
+ )
22
+ }
package/package.json CHANGED
@@ -1,6 +1,91 @@
1
1
  {
2
2
  "name": "@octane-xplat/dnd-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.10.0",
4
+ "description": "Drag and drop and sortable lists for Octane web and NativeScript",
5
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/dnd-kit",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/octane-xplat/octane-xplat",
10
+ "directory": "packages/dnd-kit"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "types",
15
+ "src",
16
+ "README.md",
17
+ "examples"
18
+ ],
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "macos": {
23
+ "types": "./types/generated/index.d.ts",
24
+ "default": "./src/index.macos.ts"
25
+ },
26
+ "web": {
27
+ "types": "./types/generated/index.d.ts",
28
+ "default": "./dist/web/index.js"
29
+ },
30
+ "native": {
31
+ "types": "./types/generated/index.d.ts",
32
+ "default": "./dist/native/index.js"
33
+ },
34
+ "types": "./types/generated/index.d.ts",
35
+ "default": "./dist/native/index.js"
36
+ }
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "@dnd-kit/abstract": "0.5.0",
43
+ "@dnd-kit/collision": "0.5.0",
44
+ "@dnd-kit/geometry": "0.5.0",
45
+ "@dnd-kit/state": "0.5.0",
46
+ "@octane-xplat/ui": "0.10.0",
47
+ "abort-controller": "3.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@nativescript-community/octane": "0.2.4",
51
+ "@nativescript/core": "9.1.2",
52
+ "@octane-xplat/macos-renderer": "0.10.0",
53
+ "@octanejs/vite-plugin": "0.1.61",
54
+ "@tsrx/typescript-plugin": "0.4.11",
55
+ "octane": "0.6.3",
56
+ "tsrx-typegen": "0.2.2",
57
+ "typescript": "5.9.3",
58
+ "vite": "8.3.0",
59
+ "vitest": "5.0.1"
60
+ },
61
+ "peerDependencies": {
62
+ "@nativescript-community/octane": ">=0.2.1 <1",
63
+ "@nativescript/core": ">=9.1.0 <10",
64
+ "octane": ">=0.6.3 <1"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@nativescript-community/octane": {
68
+ "optional": true
69
+ },
70
+ "@nativescript/core": {
71
+ "optional": true
72
+ },
73
+ "octane": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "nativescript": {
78
+ "platforms": {
79
+ "android": "9.1.0",
80
+ "ios": "9.1.0"
81
+ }
82
+ },
83
+ "scripts": {
84
+ "build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
85
+ "typegen": "tsrx-typegen --project tsconfig.json",
86
+ "typecheck": "tsrx-tsc --noEmit -p tsconfig.json && tsrx-tsc --noEmit -p tsconfig.native.json && tsrx-tsc --noEmit -p tsconfig.example.json && tsrx-tsc --noEmit -p tsconfig.macos.json",
87
+ "test": "vitest run",
88
+ "pack:check": "tsrx-typegen --pack-check",
89
+ "test:packed": "pnpm build && node tests/packed-consumer.mjs"
90
+ }
6
91
  }
@@ -0,0 +1,7 @@
1
+ import { AbortController, AbortSignal } from 'abort-controller/dist/abort-controller.js'
2
+
3
+ // The abstract core constructs AbortController at drag begin. AppKit's JSC
4
+ // host does not provide it; install only missing globals and leave other hosts intact.
5
+ const host = globalThis as any
6
+ host.AbortController ??= AbortController
7
+ host.AbortSignal ??= AbortSignal