paseo-kanban 0.1.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 +111 -0
- package/README.zh-CN.md +111 -0
- package/client/i18n.tsx +328 -0
- package/client/kanban-board.tsx +545 -0
- package/client/kanban-card.tsx +326 -0
- package/client/kanban-drag.ts +1393 -0
- package/client/kanban-lane.tsx +240 -0
- package/client/kanban-overlay.tsx +186 -0
- package/client/kanban-session.ts +696 -0
- package/client/lane-modal.tsx +267 -0
- package/client/task-modal.tsx +498 -0
- package/client/use-projects.ts +112 -0
- package/index.client.tsx +33 -0
- package/index.server.ts +7 -0
- package/package.json +41 -0
- package/paseo-plugin.json +6 -0
- package/shared/kanban.ts +465 -0
|
@@ -0,0 +1,1393 @@
|
|
|
1
|
+
import { useRef, useSyncExternalStore, useState, useEffect } from "react";
|
|
2
|
+
import type { KanbanTask } from "../shared/kanban";
|
|
3
|
+
|
|
4
|
+
export interface Rect {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LaneLayoutItem {
|
|
12
|
+
id: string;
|
|
13
|
+
rect: Rect;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DragHitTestOptions {
|
|
17
|
+
pointerX: number;
|
|
18
|
+
pointerY: number;
|
|
19
|
+
containerBounds: Rect | null;
|
|
20
|
+
scrollX: number;
|
|
21
|
+
lanes: LaneLayoutItem[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function findHoveredLaneId(options: DragHitTestOptions): string | null {
|
|
25
|
+
const { pointerX, pointerY, containerBounds, scrollX, lanes } = options;
|
|
26
|
+
|
|
27
|
+
if (containerBounds) {
|
|
28
|
+
if (
|
|
29
|
+
pointerX < containerBounds.x ||
|
|
30
|
+
pointerX > containerBounds.x + containerBounds.width ||
|
|
31
|
+
pointerY < containerBounds.y ||
|
|
32
|
+
pointerY > containerBounds.y + containerBounds.height
|
|
33
|
+
) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const containerOriginX = containerBounds ? containerBounds.x : 0;
|
|
39
|
+
const containerOriginY = containerBounds ? containerBounds.y : 0;
|
|
40
|
+
const contentX = pointerX - containerOriginX + scrollX;
|
|
41
|
+
const contentY = pointerY - containerOriginY;
|
|
42
|
+
|
|
43
|
+
for (const lane of lanes) {
|
|
44
|
+
const { x, y, width, height } = lane.rect;
|
|
45
|
+
if (
|
|
46
|
+
contentX >= x &&
|
|
47
|
+
contentX <= x + width &&
|
|
48
|
+
contentY >= y &&
|
|
49
|
+
contentY <= y + height
|
|
50
|
+
) {
|
|
51
|
+
return lane.id;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DragSlotState {
|
|
59
|
+
readonly isDragging: boolean;
|
|
60
|
+
readonly draggingTaskId: string | null;
|
|
61
|
+
readonly draggingSourceLaneId: string | null;
|
|
62
|
+
readonly hoveredLaneId: string | null;
|
|
63
|
+
readonly targetIndex: number;
|
|
64
|
+
readonly draggedCardHeight?: number;
|
|
65
|
+
readonly dragLock: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DragFeedback {
|
|
69
|
+
readonly isDragging: boolean;
|
|
70
|
+
readonly draggingTaskId: string | null;
|
|
71
|
+
readonly draggingSourceLaneId: string | null;
|
|
72
|
+
readonly hoveredLaneId: string | null;
|
|
73
|
+
readonly targetIndex?: number;
|
|
74
|
+
readonly draggedCardHeight?: number;
|
|
75
|
+
readonly pointerX?: number;
|
|
76
|
+
readonly pointerY?: number;
|
|
77
|
+
readonly autoScrollVelocity?: number;
|
|
78
|
+
readonly dragLock: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface TaskPreviewItem {
|
|
82
|
+
task: KanbanTask | null;
|
|
83
|
+
projectDisplayName: string | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface KanbanDragOptions {
|
|
87
|
+
lanes?: ({ id: string } | string)[];
|
|
88
|
+
onMoveTask?: (taskId: string, targetLaneId: string) => void | Promise<void>;
|
|
89
|
+
onReorderTask?: (taskId: string, targetLaneId: string, targetIndex: number) => void | Promise<void>;
|
|
90
|
+
getTaskPreview?: (taskId: string) => TaskPreviewItem | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ContainerRefTarget {
|
|
94
|
+
measureInWindow?: (
|
|
95
|
+
callback: (x: number, y: number, width: number, height: number) => void
|
|
96
|
+
) => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface PanResponderFactory {
|
|
100
|
+
create: (config: any) => { panHandlers: any };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface CardDragBinding {
|
|
104
|
+
readonly isDragging: boolean;
|
|
105
|
+
readonly cardPanHandlers: any;
|
|
106
|
+
readonly handlePanHandlers: any;
|
|
107
|
+
readonly onLayout: (rect: Rect) => void;
|
|
108
|
+
readonly onUnmount: () => void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface LaneDragBinding {
|
|
112
|
+
readonly isHovered: boolean;
|
|
113
|
+
readonly targetIndex: number;
|
|
114
|
+
readonly isTaskDragging: (taskId: string) => boolean;
|
|
115
|
+
readonly draggedCardHeight?: number;
|
|
116
|
+
readonly isDragLocked: () => boolean;
|
|
117
|
+
readonly bindCard: (
|
|
118
|
+
taskId: string,
|
|
119
|
+
onSelectTask?: () => void,
|
|
120
|
+
factory?: PanResponderFactory
|
|
121
|
+
) => CardDragBinding;
|
|
122
|
+
readonly registerLaneLayout: (layout: Rect) => void;
|
|
123
|
+
readonly registerCardsViewport: (layout: Rect) => void;
|
|
124
|
+
readonly handleLaneScroll: (scrollY: number) => void;
|
|
125
|
+
readonly registerCardLayout: (taskId: string, layout: Rect) => void;
|
|
126
|
+
readonly unregisterCardLayout: (taskId: string) => void;
|
|
127
|
+
readonly setDropSpacerY: (y: number | null) => void;
|
|
128
|
+
readonly setLaneCardOrder: (laneIdOrTaskIds: string | string[], taskIds?: string[]) => void;
|
|
129
|
+
readonly startGesture: (
|
|
130
|
+
taskId: string,
|
|
131
|
+
sourceLaneId: string,
|
|
132
|
+
pointerX: number,
|
|
133
|
+
pointerY: number,
|
|
134
|
+
immediate?: boolean
|
|
135
|
+
) => void;
|
|
136
|
+
readonly moveGesture: (pointerX: number, pointerY: number) => void;
|
|
137
|
+
readonly releaseGesture: (pointerX?: number, pointerY?: number) => void;
|
|
138
|
+
readonly cancelGesture: () => void;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface ActiveDragSession {
|
|
142
|
+
taskId: string;
|
|
143
|
+
sourceLaneId: string;
|
|
144
|
+
startX: number;
|
|
145
|
+
startY: number;
|
|
146
|
+
lastPointerX: number;
|
|
147
|
+
lastPointerY: number;
|
|
148
|
+
isActivated: boolean;
|
|
149
|
+
currentTargetIndex?: number;
|
|
150
|
+
cardLayouts: Map<string, Map<string, Rect>>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class KanbanDragController {
|
|
154
|
+
private lanesList: string[] = [];
|
|
155
|
+
private validLaneSet: Set<string> = new Set();
|
|
156
|
+
private laneLayouts: Map<string, Rect> = new Map();
|
|
157
|
+
private laneCardOrders: Map<string, string[]> = new Map();
|
|
158
|
+
private cardLayouts: Map<string, Map<string, Rect>> = new Map();
|
|
159
|
+
private cardsViewportLayouts: Map<string, Rect> = new Map();
|
|
160
|
+
private laneScrollY: Map<string, number> = new Map();
|
|
161
|
+
private containerBounds: Rect | null = null;
|
|
162
|
+
private containerRef: ContainerRefTarget | null = null;
|
|
163
|
+
private scrollX: number = 0;
|
|
164
|
+
private activeSession: ActiveDragSession | null = null;
|
|
165
|
+
private onMoveTask?: (taskId: string, targetLaneId: string) => void | Promise<void>;
|
|
166
|
+
private onReorderTask?: (taskId: string, targetLaneId: string, targetIndex: number) => void | Promise<void>;
|
|
167
|
+
private dragLock: boolean = false;
|
|
168
|
+
private dragLockTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
169
|
+
private pendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
170
|
+
private pendingOnPress: (() => void) | null = null;
|
|
171
|
+
private listeners: Set<() => void> = new Set();
|
|
172
|
+
private slotListeners: Set<() => void> = new Set();
|
|
173
|
+
private dropListeners: Set<() => void> = new Set();
|
|
174
|
+
private dropSpacerY: number | null = null;
|
|
175
|
+
private releaseHandler?: (releaseX?: number, releaseY?: number) => Promise<void>;
|
|
176
|
+
private cancelHandler?: () => void;
|
|
177
|
+
private panResponderFactory: PanResponderFactory | null = null;
|
|
178
|
+
private cardCallbacks: Map<string, () => void> = new Map();
|
|
179
|
+
private cardResponders: Map<string, { body: any; handle: any }> = new Map();
|
|
180
|
+
private pendingInputKind: "mouse" | "touch" = "mouse";
|
|
181
|
+
private operationSeq: number = 0;
|
|
182
|
+
private currentOperationId: string | null = null;
|
|
183
|
+
private currentDroppingState: DroppingState | null = null;
|
|
184
|
+
private getTaskPreview?: (taskId: string) => TaskPreviewItem | null;
|
|
185
|
+
|
|
186
|
+
private feedback: DragFeedback = {
|
|
187
|
+
isDragging: false,
|
|
188
|
+
draggingTaskId: null,
|
|
189
|
+
draggingSourceLaneId: null,
|
|
190
|
+
hoveredLaneId: null,
|
|
191
|
+
dragLock: false,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
private slotState: DragSlotState = {
|
|
195
|
+
isDragging: false,
|
|
196
|
+
draggingTaskId: null,
|
|
197
|
+
draggingSourceLaneId: null,
|
|
198
|
+
hoveredLaneId: null,
|
|
199
|
+
targetIndex: 0,
|
|
200
|
+
dragLock: false,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
constructor(options?: KanbanDragOptions) {
|
|
204
|
+
if (options?.lanes) {
|
|
205
|
+
this.setLanes(options.lanes);
|
|
206
|
+
}
|
|
207
|
+
if (options?.onMoveTask) {
|
|
208
|
+
this.setOnMoveTask(options.onMoveTask);
|
|
209
|
+
}
|
|
210
|
+
if (options?.onReorderTask) {
|
|
211
|
+
this.setOnReorderTask(options.onReorderTask);
|
|
212
|
+
}
|
|
213
|
+
if (options?.getTaskPreview) {
|
|
214
|
+
this.getTaskPreview = options.getTaskPreview;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setLanes = (lanes: ({ id: string } | string)[]) => {
|
|
219
|
+
this.lanesList = lanes.map((l) => (typeof l === "string" ? l : l.id));
|
|
220
|
+
this.validLaneSet = new Set(this.lanesList);
|
|
221
|
+
if (this.activeSession && this.activeSession.isActivated) {
|
|
222
|
+
this.recomputeHover(this.activeSession.lastPointerX, this.activeSession.lastPointerY);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
setOnMoveTask = (fn: (taskId: string, targetLaneId: string) => void | Promise<void>) => {
|
|
227
|
+
this.onMoveTask = fn;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
setOnReorderTask = (fn: (taskId: string, targetLaneId: string, targetIndex: number) => void | Promise<void>) => {
|
|
231
|
+
this.onReorderTask = fn;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
setGetTaskPreview = (fn?: (taskId: string) => TaskPreviewItem | null) => {
|
|
235
|
+
this.getTaskPreview = fn;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
setLaneCardOrder = (laneId: string, taskIds: string[]) => {
|
|
239
|
+
this.laneCardOrders.set(laneId, taskIds);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
registerCardLayout = (laneId: string, taskId: string, rect: Rect) => {
|
|
243
|
+
let laneMap = this.cardLayouts.get(laneId);
|
|
244
|
+
if (!laneMap) {
|
|
245
|
+
laneMap = new Map();
|
|
246
|
+
this.cardLayouts.set(laneId, laneMap);
|
|
247
|
+
}
|
|
248
|
+
laneMap.set(taskId, rect);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
unregisterCardLayout = (laneId: string, taskId: string) => {
|
|
252
|
+
const laneMap = this.cardLayouts.get(laneId);
|
|
253
|
+
if (laneMap) {
|
|
254
|
+
laneMap.delete(taskId);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
isDragLocked = (): boolean => {
|
|
259
|
+
return this.dragLock;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
registerContainerBounds = (bounds: Rect | null) => {
|
|
263
|
+
this.containerBounds = bounds;
|
|
264
|
+
if (this.activeSession) {
|
|
265
|
+
this.recomputeHover(this.activeSession.lastPointerX, this.activeSession.lastPointerY);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
bindContainerRef = (ref: ContainerRefTarget | null) => {
|
|
270
|
+
this.containerRef = ref;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
measureContainer = () => {
|
|
274
|
+
if (this.containerRef?.measureInWindow) {
|
|
275
|
+
this.containerRef.measureInWindow((wx, wy, ww, wh) => {
|
|
276
|
+
if (ww > 0 && wh > 0) {
|
|
277
|
+
this.containerBounds = { x: wx, y: wy, width: ww, height: wh };
|
|
278
|
+
if (this.activeSession) {
|
|
279
|
+
this.recomputeHover(this.activeSession.lastPointerX, this.activeSession.lastPointerY);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
handleContainerLayout = (layout: Rect) => {
|
|
287
|
+
if (!this.containerBounds) {
|
|
288
|
+
this.containerBounds = layout;
|
|
289
|
+
}
|
|
290
|
+
this.measureContainer();
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
handleScroll = (scrollX: number) => {
|
|
294
|
+
this.scrollX = scrollX;
|
|
295
|
+
if (this.activeSession) {
|
|
296
|
+
this.recomputeHover(this.activeSession.lastPointerX, this.activeSession.lastPointerY);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
registerLaneLayout = (laneId: string, rect: Rect) => {
|
|
301
|
+
this.laneLayouts.set(laneId, rect);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
registerCardsViewportLayout = (laneId: string, rect: Rect) => {
|
|
305
|
+
this.cardsViewportLayouts.set(laneId, rect);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
handleLaneScroll = (laneId: string, scrollY: number) => {
|
|
309
|
+
this.laneScrollY.set(laneId, scrollY);
|
|
310
|
+
if (this.activeSession) {
|
|
311
|
+
this.recomputeHover(this.activeSession.lastPointerX, this.activeSession.lastPointerY);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
setDropSpacerY = (y: number | null) => {
|
|
316
|
+
this.dropSpacerY = y;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
getDropSpacerY = (): number | null => {
|
|
320
|
+
return this.dropSpacerY;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
setReleaseHandler = (fn?: (releaseX?: number, releaseY?: number) => Promise<void>) => {
|
|
324
|
+
this.releaseHandler = fn;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
setCancelHandler = (fn?: () => void) => {
|
|
328
|
+
this.cancelHandler = fn;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
setPanResponderFactory = (factory: PanResponderFactory) => {
|
|
332
|
+
this.panResponderFactory = factory;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
bindCard = (
|
|
336
|
+
laneId: string,
|
|
337
|
+
taskId: string,
|
|
338
|
+
onPress?: () => void,
|
|
339
|
+
panResponderFactory?: PanResponderFactory
|
|
340
|
+
): CardDragBinding => {
|
|
341
|
+
const key = `${laneId}:${taskId}`;
|
|
342
|
+
if (onPress) {
|
|
343
|
+
this.cardCallbacks.set(key, onPress);
|
|
344
|
+
} else {
|
|
345
|
+
this.cardCallbacks.delete(key);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const factory = panResponderFactory ?? this.panResponderFactory;
|
|
349
|
+
let responders = this.cardResponders.get(key);
|
|
350
|
+
if (!responders && factory) {
|
|
351
|
+
const body = factory.create({
|
|
352
|
+
onStartShouldSetPanResponder: () => true,
|
|
353
|
+
onMoveShouldSetPanResponder: () => true,
|
|
354
|
+
onPanResponderGrant: (_e: any, gs: any) => {
|
|
355
|
+
const cb = this.cardCallbacks.get(key);
|
|
356
|
+
this.handlePointerDown(
|
|
357
|
+
{ taskId, laneId, onPress: cb },
|
|
358
|
+
{ x: gs.x0, y: gs.y0 },
|
|
359
|
+
"body"
|
|
360
|
+
);
|
|
361
|
+
},
|
|
362
|
+
onPanResponderMove: (_e: any, gs: any) => {
|
|
363
|
+
this.handlePointerMove({ x: gs.moveX, y: gs.moveY });
|
|
364
|
+
},
|
|
365
|
+
onPanResponderRelease: () => this.handlePointerUp(),
|
|
366
|
+
onPanResponderTerminate: () => {
|
|
367
|
+
this.handlePointerCancel();
|
|
368
|
+
},
|
|
369
|
+
onPanResponderTerminationRequest: () => !this.isTaskDragging(taskId),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const handle = factory.create({
|
|
373
|
+
onStartShouldSetPanResponder: () => true,
|
|
374
|
+
onMoveShouldSetPanResponder: () => true,
|
|
375
|
+
onPanResponderGrant: (_e: any, gs: any) => {
|
|
376
|
+
const cb = this.cardCallbacks.get(key);
|
|
377
|
+
this.handlePointerDown(
|
|
378
|
+
{ taskId, laneId, onPress: cb },
|
|
379
|
+
{ x: gs.x0, y: gs.y0 },
|
|
380
|
+
"handle"
|
|
381
|
+
);
|
|
382
|
+
},
|
|
383
|
+
onPanResponderMove: (_e: any, gs: any) => {
|
|
384
|
+
this.handlePointerMove({ x: gs.moveX, y: gs.moveY });
|
|
385
|
+
},
|
|
386
|
+
onPanResponderRelease: () => this.handlePointerUp(),
|
|
387
|
+
onPanResponderTerminate: () => {
|
|
388
|
+
this.handlePointerCancel();
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
responders = { body, handle };
|
|
393
|
+
this.cardResponders.set(key, responders);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
isDragging: this.isTaskDragging(taskId),
|
|
398
|
+
cardPanHandlers: responders?.body?.panHandlers,
|
|
399
|
+
handlePanHandlers: responders?.handle?.panHandlers,
|
|
400
|
+
onLayout: (rect: Rect) => this.registerCardLayout(laneId, taskId, rect),
|
|
401
|
+
onUnmount: () => {
|
|
402
|
+
this.cardCallbacks.delete(key);
|
|
403
|
+
this.cardResponders.delete(key);
|
|
404
|
+
this.unregisterCardLayout(laneId, taskId);
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
private clearPendingTimer = () => {
|
|
410
|
+
if (this.pendingTimer) {
|
|
411
|
+
clearTimeout(this.pendingTimer);
|
|
412
|
+
this.pendingTimer = null;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
handlePointerDown = (
|
|
417
|
+
context: { taskId: string; laneId: string; onPress?: () => void },
|
|
418
|
+
point: { x: number; y: number },
|
|
419
|
+
source: "handle" | "body" = "body",
|
|
420
|
+
inputKind?: "mouse" | "touch"
|
|
421
|
+
) => {
|
|
422
|
+
if (this.dragLock) return;
|
|
423
|
+
if (this.activeSession?.isActivated) return;
|
|
424
|
+
if (this.currentDroppingState) return;
|
|
425
|
+
|
|
426
|
+
this.clearPendingTimer();
|
|
427
|
+
this.pendingOnPress = context.onPress ?? null;
|
|
428
|
+
this.pendingInputKind = inputKind ?? "mouse";
|
|
429
|
+
|
|
430
|
+
if (source === "handle") {
|
|
431
|
+
this.startGesture(context.taskId, context.laneId, point.x, point.y, true);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
this.startGesture(context.taskId, context.laneId, point.x, point.y, false);
|
|
436
|
+
|
|
437
|
+
const longPressDuration = inputKind === "touch" ? 220 : 260;
|
|
438
|
+
this.pendingTimer = setTimeout(() => {
|
|
439
|
+
this.pendingTimer = null;
|
|
440
|
+
if (this.dragLock) return;
|
|
441
|
+
if (this.activeSession && !this.activeSession.isActivated) {
|
|
442
|
+
this.activeSession.isActivated = true;
|
|
443
|
+
this.dragLock = true;
|
|
444
|
+
const initialHit = this.computeHit(
|
|
445
|
+
this.activeSession.lastPointerX,
|
|
446
|
+
this.activeSession.lastPointerY
|
|
447
|
+
);
|
|
448
|
+
const targetLaneId = initialHit ?? this.activeSession.sourceLaneId;
|
|
449
|
+
const targetIndex = this.computeTargetIndex(
|
|
450
|
+
targetLaneId,
|
|
451
|
+
this.activeSession.lastPointerY
|
|
452
|
+
);
|
|
453
|
+
this.activeSession.currentTargetIndex = targetIndex;
|
|
454
|
+
this.feedback = {
|
|
455
|
+
...this.feedback,
|
|
456
|
+
isDragging: true,
|
|
457
|
+
draggingTaskId: this.activeSession.taskId,
|
|
458
|
+
draggingSourceLaneId: this.activeSession.sourceLaneId,
|
|
459
|
+
hoveredLaneId: targetLaneId,
|
|
460
|
+
targetIndex,
|
|
461
|
+
draggedCardHeight: this.activeSession.cardLayouts
|
|
462
|
+
.get(this.activeSession.sourceLaneId)
|
|
463
|
+
?.get(this.activeSession.taskId)?.height,
|
|
464
|
+
dragLock: true,
|
|
465
|
+
};
|
|
466
|
+
this.notifyListeners();
|
|
467
|
+
}
|
|
468
|
+
}, longPressDuration);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
handlePointerMove = (point: { x: number; y: number }) => {
|
|
472
|
+
if (!this.activeSession) return;
|
|
473
|
+
const dist = Math.hypot(
|
|
474
|
+
point.x - this.activeSession.startX,
|
|
475
|
+
point.y - this.activeSession.startY
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// If unactivated touch gesture and user moved >= 5px before 220ms:
|
|
479
|
+
// User is scrolling! Cancel pending gesture, do NOT activate drag, do NOT trigger tap!
|
|
480
|
+
if (!this.activeSession.isActivated && this.pendingInputKind === "touch") {
|
|
481
|
+
if (dist >= 5) {
|
|
482
|
+
this.clearPendingTimer();
|
|
483
|
+
this.pendingOnPress = null;
|
|
484
|
+
this.cancelGesture();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (dist >= 5) {
|
|
490
|
+
this.clearPendingTimer();
|
|
491
|
+
}
|
|
492
|
+
this.moveGesture(point.x, point.y);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
handlePointerUp = async (point?: { x: number; y: number }): Promise<void> => {
|
|
496
|
+
this.clearPendingTimer();
|
|
497
|
+
if (!this.activeSession) return;
|
|
498
|
+
|
|
499
|
+
point ??= {
|
|
500
|
+
x: this.activeSession.lastPointerX,
|
|
501
|
+
y: this.activeSession.lastPointerY,
|
|
502
|
+
};
|
|
503
|
+
const wasActivated = this.activeSession.isActivated;
|
|
504
|
+
const dist = Math.hypot(
|
|
505
|
+
point.x - this.activeSession.startX,
|
|
506
|
+
point.y - this.activeSession.startY
|
|
507
|
+
);
|
|
508
|
+
const onPress = this.pendingOnPress;
|
|
509
|
+
this.pendingOnPress = null;
|
|
510
|
+
|
|
511
|
+
if (wasActivated) {
|
|
512
|
+
if (this.releaseHandler) {
|
|
513
|
+
await this.releaseHandler(point?.x, point?.y);
|
|
514
|
+
} else {
|
|
515
|
+
await this.releaseGesture(point?.x, point?.y);
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
await this.releaseGesture(point?.x, point?.y);
|
|
519
|
+
if (dist < 5 && !this.dragLock && onPress) {
|
|
520
|
+
onPress();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
handlePointerCancel = () => {
|
|
526
|
+
this.clearPendingTimer();
|
|
527
|
+
this.pendingOnPress = null;
|
|
528
|
+
if (this.cancelHandler) {
|
|
529
|
+
this.cancelHandler();
|
|
530
|
+
} else {
|
|
531
|
+
this.cancelGesture();
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
startGesture = (
|
|
536
|
+
taskId: string,
|
|
537
|
+
sourceLaneId: string,
|
|
538
|
+
pointerX: number,
|
|
539
|
+
pointerY: number,
|
|
540
|
+
immediate: boolean = true
|
|
541
|
+
) => {
|
|
542
|
+
if (this.dragLockTimeout) {
|
|
543
|
+
clearTimeout(this.dragLockTimeout);
|
|
544
|
+
this.dragLockTimeout = null;
|
|
545
|
+
}
|
|
546
|
+
this.measureContainer();
|
|
547
|
+
|
|
548
|
+
const isActivated = immediate;
|
|
549
|
+
if (isActivated) {
|
|
550
|
+
this.dragLock = true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this.activeSession = {
|
|
554
|
+
taskId,
|
|
555
|
+
sourceLaneId,
|
|
556
|
+
startX: pointerX,
|
|
557
|
+
startY: pointerY,
|
|
558
|
+
lastPointerX: pointerX,
|
|
559
|
+
lastPointerY: pointerY,
|
|
560
|
+
isActivated,
|
|
561
|
+
currentTargetIndex: undefined,
|
|
562
|
+
cardLayouts: new Map([...this.cardLayouts].map(([id, cards]) => [id, new Map(cards)])),
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
if (isActivated) {
|
|
566
|
+
const initialHit = this.computeHit(pointerX, pointerY);
|
|
567
|
+
const targetLaneId = initialHit ?? sourceLaneId;
|
|
568
|
+
const targetIndex = this.computeTargetIndex(targetLaneId, pointerY);
|
|
569
|
+
this.activeSession.currentTargetIndex = targetIndex;
|
|
570
|
+
const autoScrollVelocity = this.computeAutoScrollVelocity(pointerX);
|
|
571
|
+
|
|
572
|
+
this.feedback = {
|
|
573
|
+
isDragging: true,
|
|
574
|
+
draggingTaskId: taskId,
|
|
575
|
+
draggingSourceLaneId: sourceLaneId,
|
|
576
|
+
hoveredLaneId: targetLaneId,
|
|
577
|
+
targetIndex,
|
|
578
|
+
draggedCardHeight: this.activeSession.cardLayouts.get(sourceLaneId)?.get(taskId)?.height,
|
|
579
|
+
pointerX,
|
|
580
|
+
pointerY,
|
|
581
|
+
autoScrollVelocity,
|
|
582
|
+
dragLock: true,
|
|
583
|
+
};
|
|
584
|
+
} else {
|
|
585
|
+
this.feedback = {
|
|
586
|
+
isDragging: false,
|
|
587
|
+
draggingTaskId: taskId,
|
|
588
|
+
draggingSourceLaneId: sourceLaneId,
|
|
589
|
+
hoveredLaneId: null,
|
|
590
|
+
targetIndex: undefined,
|
|
591
|
+
pointerX,
|
|
592
|
+
pointerY,
|
|
593
|
+
autoScrollVelocity: 0,
|
|
594
|
+
dragLock: false,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
this.notifyListeners();
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
moveGesture = (pointerX: number, pointerY: number) => {
|
|
601
|
+
if (!this.activeSession) return;
|
|
602
|
+
|
|
603
|
+
if (pointerX > 0 || pointerY > 0) {
|
|
604
|
+
this.activeSession.lastPointerX = pointerX;
|
|
605
|
+
this.activeSession.lastPointerY = pointerY;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!this.activeSession.isActivated) {
|
|
609
|
+
const dist = Math.hypot(
|
|
610
|
+
this.activeSession.lastPointerX - this.activeSession.startX,
|
|
611
|
+
this.activeSession.lastPointerY - this.activeSession.startY
|
|
612
|
+
);
|
|
613
|
+
if (dist >= 5) {
|
|
614
|
+
this.activeSession.isActivated = true;
|
|
615
|
+
this.dragLock = true;
|
|
616
|
+
} else {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const hitLaneId = this.computeHit(
|
|
622
|
+
this.activeSession.lastPointerX,
|
|
623
|
+
this.activeSession.lastPointerY
|
|
624
|
+
);
|
|
625
|
+
const targetLaneId = hitLaneId ?? this.activeSession.sourceLaneId;
|
|
626
|
+
const targetIndex = this.computeTargetIndex(targetLaneId, this.activeSession.lastPointerY);
|
|
627
|
+
this.activeSession.currentTargetIndex = targetIndex;
|
|
628
|
+
const autoScrollVelocity = this.computeAutoScrollVelocity(this.activeSession.lastPointerX);
|
|
629
|
+
|
|
630
|
+
this.feedback = {
|
|
631
|
+
isDragging: true,
|
|
632
|
+
draggingTaskId: this.activeSession.taskId,
|
|
633
|
+
draggingSourceLaneId: this.activeSession.sourceLaneId,
|
|
634
|
+
hoveredLaneId: hitLaneId,
|
|
635
|
+
targetIndex,
|
|
636
|
+
draggedCardHeight: this.activeSession.cardLayouts.get(this.activeSession.sourceLaneId)?.get(this.activeSession.taskId)?.height,
|
|
637
|
+
pointerX: this.activeSession.lastPointerX,
|
|
638
|
+
pointerY: this.activeSession.lastPointerY,
|
|
639
|
+
autoScrollVelocity,
|
|
640
|
+
dragLock: true,
|
|
641
|
+
};
|
|
642
|
+
this.notifyListeners();
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
releaseGesture = async (pointerX?: number, pointerY?: number): Promise<void> => {
|
|
646
|
+
const session = this.activeSession;
|
|
647
|
+
if (!session) return;
|
|
648
|
+
|
|
649
|
+
if (!session.isActivated) {
|
|
650
|
+
this.activeSession = null;
|
|
651
|
+
this.dragLock = false;
|
|
652
|
+
this.feedback = {
|
|
653
|
+
isDragging: false,
|
|
654
|
+
draggingTaskId: null,
|
|
655
|
+
draggingSourceLaneId: null,
|
|
656
|
+
hoveredLaneId: null,
|
|
657
|
+
targetIndex: undefined,
|
|
658
|
+
pointerX: undefined,
|
|
659
|
+
pointerY: undefined,
|
|
660
|
+
autoScrollVelocity: 0,
|
|
661
|
+
dragLock: false,
|
|
662
|
+
};
|
|
663
|
+
this.notifyListeners();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const drop = await this.beginDropAnimation({
|
|
668
|
+
x: pointerX ?? session.lastPointerX,
|
|
669
|
+
y: pointerY ?? session.lastPointerY,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
if (drop) {
|
|
673
|
+
await this.reportDropComplete(drop.operationId);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
beginDropAnimation = async (point?: { x?: number; y?: number }): Promise<DroppingState | null> => {
|
|
678
|
+
const session = this.activeSession;
|
|
679
|
+
if (!session || !session.isActivated) {
|
|
680
|
+
this.clearPendingTimer();
|
|
681
|
+
this.cancelGesture();
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const finalX =
|
|
686
|
+
typeof point?.x === "number" && !isNaN(point.x) && point.x > 0
|
|
687
|
+
? point.x
|
|
688
|
+
: session.lastPointerX;
|
|
689
|
+
const finalY =
|
|
690
|
+
typeof point?.y === "number" && !isNaN(point.y) && point.y > 0
|
|
691
|
+
? point.y
|
|
692
|
+
: session.lastPointerY;
|
|
693
|
+
|
|
694
|
+
const hitLaneId = this.computeHit(finalX, finalY);
|
|
695
|
+
const targetLaneId = hitLaneId ?? session.sourceLaneId;
|
|
696
|
+
const targetIndex = this.computeTargetIndex(targetLaneId, finalY);
|
|
697
|
+
|
|
698
|
+
const isValidTarget = Boolean(hitLaneId && this.validLaneSet.has(hitLaneId));
|
|
699
|
+
const isDrop = isValidTarget;
|
|
700
|
+
|
|
701
|
+
const opId = `op_${++this.operationSeq}_${Date.now()}`;
|
|
702
|
+
this.currentOperationId = opId;
|
|
703
|
+
|
|
704
|
+
const containerX = this.containerBounds?.x ?? 0;
|
|
705
|
+
const containerY = this.containerBounds?.y ?? 0;
|
|
706
|
+
const cardHalfWidth = 140;
|
|
707
|
+
|
|
708
|
+
const fromX = finalX - containerX - cardHalfWidth;
|
|
709
|
+
const fromY = finalY - containerY - 20;
|
|
710
|
+
|
|
711
|
+
let toX = fromX;
|
|
712
|
+
let toY = fromY;
|
|
713
|
+
|
|
714
|
+
if (isDrop) {
|
|
715
|
+
const targetSlot = this.getDropSlotPosition(targetLaneId, targetIndex);
|
|
716
|
+
if (targetSlot) {
|
|
717
|
+
toX = targetSlot.x;
|
|
718
|
+
const laneScroll = this.getLaneScroll(targetLaneId);
|
|
719
|
+
const laneLayout = this.getLaneLayout(targetLaneId);
|
|
720
|
+
const cardsViewport = this.getCardsViewportLayout(targetLaneId);
|
|
721
|
+
const dropSpacerY = this.getDropSpacerY();
|
|
722
|
+
if (dropSpacerY !== null && hitLaneId === targetLaneId) {
|
|
723
|
+
toY =
|
|
724
|
+
(laneLayout?.y ?? 0) +
|
|
725
|
+
(cardsViewport?.y ?? 0) -
|
|
726
|
+
laneScroll +
|
|
727
|
+
dropSpacerY;
|
|
728
|
+
} else {
|
|
729
|
+
toY = targetSlot.y;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
const sourceIndex = Math.max(0, this.laneCardOrders.get(session.sourceLaneId)?.indexOf(session.taskId) ?? 0);
|
|
734
|
+
const sourceSlot = this.getDropSlotPosition(session.sourceLaneId, sourceIndex);
|
|
735
|
+
if (sourceSlot) {
|
|
736
|
+
toX = sourceSlot.x;
|
|
737
|
+
toY = sourceSlot.y;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const preview = this.getTaskPreview ? this.getTaskPreview(session.taskId) : null;
|
|
742
|
+
|
|
743
|
+
const droppingState: DroppingState = {
|
|
744
|
+
operationId: opId,
|
|
745
|
+
taskId: session.taskId,
|
|
746
|
+
sourceLaneId: session.sourceLaneId,
|
|
747
|
+
task: preview?.task ?? null,
|
|
748
|
+
projectName: preview?.projectDisplayName ?? null,
|
|
749
|
+
targetLaneId,
|
|
750
|
+
targetIndex,
|
|
751
|
+
isDrop,
|
|
752
|
+
fromX,
|
|
753
|
+
fromY,
|
|
754
|
+
toX,
|
|
755
|
+
toY,
|
|
756
|
+
releaseX: finalX,
|
|
757
|
+
releaseY: finalY,
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const savedCardHeight = this.feedback.draggedCardHeight;
|
|
761
|
+
this.currentDroppingState = droppingState;
|
|
762
|
+
this.activeSession = null;
|
|
763
|
+
this.dragLock = true;
|
|
764
|
+
this.feedback = {
|
|
765
|
+
isDragging: false,
|
|
766
|
+
draggingTaskId: null,
|
|
767
|
+
draggingSourceLaneId: null,
|
|
768
|
+
hoveredLaneId: null,
|
|
769
|
+
targetIndex: undefined,
|
|
770
|
+
pointerX: undefined,
|
|
771
|
+
pointerY: undefined,
|
|
772
|
+
autoScrollVelocity: 0,
|
|
773
|
+
dragLock: true,
|
|
774
|
+
draggedCardHeight: savedCardHeight,
|
|
775
|
+
};
|
|
776
|
+
this.notifyListeners();
|
|
777
|
+
this.notifyDropListeners();
|
|
778
|
+
return droppingState;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
reportDropComplete = async (operationId: string): Promise<void> => {
|
|
782
|
+
if (!this.currentOperationId || this.currentOperationId !== operationId) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const drop = this.currentDroppingState;
|
|
787
|
+
this.currentOperationId = null;
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
if (drop && drop.isDrop && this.validLaneSet.has(drop.targetLaneId)) {
|
|
791
|
+
if (this.onReorderTask) {
|
|
792
|
+
await this.onReorderTask(drop.taskId, drop.targetLaneId, drop.targetIndex);
|
|
793
|
+
} else if (this.onMoveTask && drop.targetLaneId !== drop.sourceLaneId) {
|
|
794
|
+
await this.onMoveTask(drop.taskId, drop.targetLaneId);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
} finally {
|
|
798
|
+
this.currentDroppingState = null;
|
|
799
|
+
this.dropSpacerY = null;
|
|
800
|
+
this.notifyDropListeners();
|
|
801
|
+
this.notifyListeners();
|
|
802
|
+
|
|
803
|
+
if (this.dragLockTimeout) clearTimeout(this.dragLockTimeout);
|
|
804
|
+
this.dragLockTimeout = setTimeout(() => {
|
|
805
|
+
this.dragLock = false;
|
|
806
|
+
this.feedback = {
|
|
807
|
+
...this.feedback,
|
|
808
|
+
dragLock: false,
|
|
809
|
+
};
|
|
810
|
+
this.notifyListeners();
|
|
811
|
+
}, 60);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
abortDrop = (operationId: string) => {
|
|
816
|
+
if (!this.currentOperationId || this.currentOperationId !== operationId) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
this.currentOperationId = null;
|
|
820
|
+
this.currentDroppingState = null;
|
|
821
|
+
this.dropSpacerY = null;
|
|
822
|
+
this.notifyDropListeners();
|
|
823
|
+
this.notifyListeners();
|
|
824
|
+
|
|
825
|
+
if (this.dragLockTimeout) clearTimeout(this.dragLockTimeout);
|
|
826
|
+
this.dragLockTimeout = setTimeout(() => {
|
|
827
|
+
this.dragLock = false;
|
|
828
|
+
this.feedback = {
|
|
829
|
+
...this.feedback,
|
|
830
|
+
dragLock: false,
|
|
831
|
+
};
|
|
832
|
+
this.notifyListeners();
|
|
833
|
+
}, 60);
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
cancelGesture = () => {
|
|
837
|
+
this.clearPendingTimer();
|
|
838
|
+
this.pendingOnPress = null;
|
|
839
|
+
if (!this.activeSession) return;
|
|
840
|
+
const wasActivated = this.activeSession.isActivated;
|
|
841
|
+
this.activeSession = null;
|
|
842
|
+
this.feedback = {
|
|
843
|
+
isDragging: false,
|
|
844
|
+
draggingTaskId: null,
|
|
845
|
+
draggingSourceLaneId: null,
|
|
846
|
+
hoveredLaneId: null,
|
|
847
|
+
targetIndex: undefined,
|
|
848
|
+
pointerX: undefined,
|
|
849
|
+
pointerY: undefined,
|
|
850
|
+
autoScrollVelocity: 0,
|
|
851
|
+
dragLock: wasActivated,
|
|
852
|
+
};
|
|
853
|
+
this.notifyListeners();
|
|
854
|
+
|
|
855
|
+
if (wasActivated) {
|
|
856
|
+
if (this.dragLockTimeout) clearTimeout(this.dragLockTimeout);
|
|
857
|
+
this.dragLockTimeout = setTimeout(() => {
|
|
858
|
+
this.dragLock = false;
|
|
859
|
+
this.feedback = {
|
|
860
|
+
...this.feedback,
|
|
861
|
+
dragLock: false,
|
|
862
|
+
};
|
|
863
|
+
this.notifyListeners();
|
|
864
|
+
}, 60);
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
getContainerBounds = (): Rect | null => {
|
|
869
|
+
return this.containerBounds;
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
getLaneLayout = (laneId: string): Rect | undefined => {
|
|
873
|
+
return this.laneLayouts.get(laneId);
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
getCardsViewportLayout = (laneId: string): Rect | undefined => {
|
|
877
|
+
return this.cardsViewportLayouts.get(laneId);
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
getLaneScroll = (laneId: string): number => {
|
|
881
|
+
return this.laneScrollY.get(laneId) ?? 0;
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
getDropSlotPosition = (
|
|
885
|
+
laneId: string,
|
|
886
|
+
targetIndex: number
|
|
887
|
+
): { x: number; y: number } | null => {
|
|
888
|
+
const laneRect = this.laneLayouts.get(laneId);
|
|
889
|
+
if (!laneRect) return null;
|
|
890
|
+
|
|
891
|
+
const cardsViewport = this.cardsViewportLayouts.get(laneId);
|
|
892
|
+
const scrollY = this.laneScrollY.get(laneId) ?? 0;
|
|
893
|
+
const laneCardsMap = (this.activeSession?.cardLayouts ?? this.cardLayouts).get(laneId);
|
|
894
|
+
|
|
895
|
+
const slotX = laneRect.x - this.scrollX + (cardsViewport?.x ?? 12);
|
|
896
|
+
const activeTaskId =
|
|
897
|
+
this.activeSession?.taskId ??
|
|
898
|
+
this.feedback.draggingTaskId ??
|
|
899
|
+
this.currentDroppingState?.taskId;
|
|
900
|
+
const allCardIds = this.laneCardOrders.get(laneId) ?? [];
|
|
901
|
+
const remainingCardIds = allCardIds.filter((id) => id !== activeTaskId);
|
|
902
|
+
|
|
903
|
+
const laneOriginY = laneRect.y;
|
|
904
|
+
const cardsOriginY = cardsViewport?.y ?? 0;
|
|
905
|
+
|
|
906
|
+
let slotCardY = 0;
|
|
907
|
+
if (remainingCardIds.length === 0 || targetIndex <= 0) {
|
|
908
|
+
slotCardY = 0;
|
|
909
|
+
} else if (targetIndex < remainingCardIds.length) {
|
|
910
|
+
const nextCardId = remainingCardIds[targetIndex];
|
|
911
|
+
const nextCardRect = laneCardsMap?.get(nextCardId);
|
|
912
|
+
if (nextCardRect) {
|
|
913
|
+
slotCardY = nextCardRect.y;
|
|
914
|
+
} else {
|
|
915
|
+
const prevCardId = remainingCardIds[targetIndex - 1];
|
|
916
|
+
const prevRect = laneCardsMap?.get(prevCardId);
|
|
917
|
+
slotCardY = prevRect ? prevRect.y + prevRect.height + 8 : targetIndex * 80;
|
|
918
|
+
}
|
|
919
|
+
} else {
|
|
920
|
+
const lastCardId = remainingCardIds[remainingCardIds.length - 1];
|
|
921
|
+
const lastCardRect = laneCardsMap?.get(lastCardId);
|
|
922
|
+
slotCardY = lastCardRect ? lastCardRect.y + lastCardRect.height + 8 : remainingCardIds.length * 80;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const slotY = laneOriginY + cardsOriginY - scrollY + slotCardY;
|
|
926
|
+
return { x: slotX, y: slotY };
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
getFeedback = (): DragFeedback => {
|
|
930
|
+
return this.feedback;
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
getSlotState = (): DragSlotState => {
|
|
934
|
+
return this.slotState;
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
isTaskDragging = (taskId: string): boolean => {
|
|
938
|
+
if (this.currentDroppingState && this.currentDroppingState.taskId === taskId) {
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
return this.feedback.isDragging && this.feedback.draggingTaskId === taskId;
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
isLaneHovered = (laneId: string): boolean => {
|
|
945
|
+
if (this.currentDroppingState) {
|
|
946
|
+
return this.currentDroppingState.targetLaneId === laneId;
|
|
947
|
+
}
|
|
948
|
+
return this.feedback.hoveredLaneId === laneId && this.feedback.isDragging;
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
952
|
+
this.listeners.add(listener);
|
|
953
|
+
return () => {
|
|
954
|
+
this.listeners.delete(listener);
|
|
955
|
+
};
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
subscribeSlot = (listener: () => void): (() => void) => {
|
|
959
|
+
this.slotListeners.add(listener);
|
|
960
|
+
return () => {
|
|
961
|
+
this.slotListeners.delete(listener);
|
|
962
|
+
};
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
subscribeDrop = (listener: () => void): (() => void) => {
|
|
966
|
+
this.dropListeners.add(listener);
|
|
967
|
+
return () => {
|
|
968
|
+
this.dropListeners.delete(listener);
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
getDroppingState = (): DroppingState | null => {
|
|
973
|
+
return this.currentDroppingState;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
private notifyDropListeners() {
|
|
977
|
+
for (const listener of this.dropListeners) {
|
|
978
|
+
listener();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
private checkAndNotifySlotState() {
|
|
983
|
+
const isDropping = this.currentDroppingState !== null;
|
|
984
|
+
const nextSlotState: DragSlotState = {
|
|
985
|
+
isDragging: this.feedback.isDragging || isDropping,
|
|
986
|
+
draggingTaskId:
|
|
987
|
+
this.feedback.draggingTaskId ??
|
|
988
|
+
(isDropping ? this.currentDroppingState?.taskId ?? null : null),
|
|
989
|
+
draggingSourceLaneId:
|
|
990
|
+
this.feedback.draggingSourceLaneId ??
|
|
991
|
+
(isDropping ? this.currentDroppingState?.sourceLaneId ?? null : null),
|
|
992
|
+
hoveredLaneId:
|
|
993
|
+
this.feedback.hoveredLaneId ??
|
|
994
|
+
(isDropping ? this.currentDroppingState?.targetLaneId ?? null : null),
|
|
995
|
+
targetIndex:
|
|
996
|
+
this.feedback.targetIndex ??
|
|
997
|
+
(isDropping ? this.currentDroppingState?.targetIndex ?? 0 : 0),
|
|
998
|
+
draggedCardHeight: this.feedback.draggedCardHeight,
|
|
999
|
+
dragLock: this.feedback.dragLock,
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
const slotChanged =
|
|
1003
|
+
this.slotState.isDragging !== nextSlotState.isDragging ||
|
|
1004
|
+
this.slotState.draggingTaskId !== nextSlotState.draggingTaskId ||
|
|
1005
|
+
this.slotState.draggingSourceLaneId !== nextSlotState.draggingSourceLaneId ||
|
|
1006
|
+
this.slotState.hoveredLaneId !== nextSlotState.hoveredLaneId ||
|
|
1007
|
+
this.slotState.targetIndex !== nextSlotState.targetIndex ||
|
|
1008
|
+
this.slotState.draggedCardHeight !== nextSlotState.draggedCardHeight ||
|
|
1009
|
+
this.slotState.dragLock !== nextSlotState.dragLock;
|
|
1010
|
+
|
|
1011
|
+
if (slotChanged) {
|
|
1012
|
+
this.slotState = nextSlotState;
|
|
1013
|
+
for (const listener of this.slotListeners) {
|
|
1014
|
+
listener();
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private notifyListeners() {
|
|
1020
|
+
this.checkAndNotifySlotState();
|
|
1021
|
+
for (const listener of this.listeners) {
|
|
1022
|
+
listener();
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private computeAutoScrollVelocity(pointerX: number): number {
|
|
1027
|
+
if (!this.containerBounds) return 0;
|
|
1028
|
+
const relX = pointerX - this.containerBounds.x;
|
|
1029
|
+
if (relX < 48) {
|
|
1030
|
+
return Math.max(-1, Math.min(0, (relX - 48) / 48));
|
|
1031
|
+
}
|
|
1032
|
+
if (relX > this.containerBounds.width - 48) {
|
|
1033
|
+
return Math.min(1, Math.max(0, (relX - (this.containerBounds.width - 48)) / 48));
|
|
1034
|
+
}
|
|
1035
|
+
return 0;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private computeTargetIndex(laneId: string, pointerY: number): number {
|
|
1039
|
+
const allCardIds = this.laneCardOrders.get(laneId) ?? [];
|
|
1040
|
+
const activeTaskId = this.activeSession?.taskId;
|
|
1041
|
+
const isSourceLane = this.activeSession ? this.activeSession.sourceLaneId === laneId : false;
|
|
1042
|
+
|
|
1043
|
+
const remainingCount = isSourceLane && allCardIds.includes(activeTaskId ?? "")
|
|
1044
|
+
? Math.max(0, allCardIds.length - 1)
|
|
1045
|
+
: allCardIds.length;
|
|
1046
|
+
|
|
1047
|
+
if (allCardIds.length === 0 || remainingCount === 0) return 0;
|
|
1048
|
+
|
|
1049
|
+
const laneRect = this.laneLayouts.get(laneId);
|
|
1050
|
+
const containerOriginY = this.containerBounds?.y ?? 0;
|
|
1051
|
+
const laneOriginY = laneRect?.y ?? 0;
|
|
1052
|
+
const cardsOriginY = this.cardsViewportLayouts.get(laneId)?.y ?? 0;
|
|
1053
|
+
const scrollY = this.laneScrollY.get(laneId) ?? 0;
|
|
1054
|
+
const laneCardsMap = (this.activeSession?.cardLayouts ?? this.cardLayouts).get(laneId);
|
|
1055
|
+
|
|
1056
|
+
const prevIndex = this.feedback.hoveredLaneId === laneId
|
|
1057
|
+
? this.activeSession?.currentTargetIndex ?? -1
|
|
1058
|
+
: -1;
|
|
1059
|
+
|
|
1060
|
+
for (let i = 0; i < allCardIds.length; i++) {
|
|
1061
|
+
const cardId = allCardIds[i];
|
|
1062
|
+
const cardRect = laneCardsMap?.get(cardId);
|
|
1063
|
+
if (!cardRect) continue;
|
|
1064
|
+
|
|
1065
|
+
const cardTop = containerOriginY + laneOriginY + cardsOriginY - scrollY + cardRect.y;
|
|
1066
|
+
const cardBottom = cardTop + cardRect.height;
|
|
1067
|
+
|
|
1068
|
+
let thresholdY: number;
|
|
1069
|
+
if (i < allCardIds.length - 1) {
|
|
1070
|
+
const nextCardId = allCardIds[i + 1];
|
|
1071
|
+
const nextCardRect = laneCardsMap?.get(nextCardId);
|
|
1072
|
+
if (nextCardRect) {
|
|
1073
|
+
const nextCardTop = containerOriginY + laneOriginY + cardsOriginY - scrollY + nextCardRect.y;
|
|
1074
|
+
thresholdY = (cardBottom + nextCardTop) / 2;
|
|
1075
|
+
} else {
|
|
1076
|
+
thresholdY = cardBottom + 4;
|
|
1077
|
+
}
|
|
1078
|
+
} else {
|
|
1079
|
+
thresholdY = cardBottom + 4;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const lowerBuffer = thresholdY - 8;
|
|
1083
|
+
const upperBuffer = thresholdY + 8;
|
|
1084
|
+
|
|
1085
|
+
if (pointerY < lowerBuffer) {
|
|
1086
|
+
return Math.min(i, remainingCount);
|
|
1087
|
+
}
|
|
1088
|
+
if (pointerY <= upperBuffer) {
|
|
1089
|
+
if (prevIndex === i) {
|
|
1090
|
+
return Math.min(i, remainingCount);
|
|
1091
|
+
}
|
|
1092
|
+
if (prevIndex === i + 1) {
|
|
1093
|
+
return Math.min(i + 1, remainingCount);
|
|
1094
|
+
}
|
|
1095
|
+
return pointerY >= thresholdY
|
|
1096
|
+
? Math.min(i + 1, remainingCount)
|
|
1097
|
+
: Math.min(i, remainingCount);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return remainingCount;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private computeHit(pointerX: number, pointerY: number): string | null {
|
|
1105
|
+
const laneItems: LaneLayoutItem[] = [];
|
|
1106
|
+
for (const laneId of this.lanesList) {
|
|
1107
|
+
const rect = this.laneLayouts.get(laneId);
|
|
1108
|
+
if (rect) {
|
|
1109
|
+
laneItems.push({ id: laneId, rect });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const hitId = findHoveredLaneId({
|
|
1114
|
+
pointerX,
|
|
1115
|
+
pointerY,
|
|
1116
|
+
containerBounds: this.containerBounds,
|
|
1117
|
+
scrollX: this.scrollX,
|
|
1118
|
+
lanes: laneItems,
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
if (hitId && this.validLaneSet.has(hitId)) {
|
|
1122
|
+
return hitId;
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private recomputeHover(pointerX: number, pointerY: number) {
|
|
1128
|
+
if (!this.activeSession?.isActivated) return;
|
|
1129
|
+
const hitLaneId = this.computeHit(pointerX, pointerY);
|
|
1130
|
+
const targetIndex = hitLaneId ? this.computeTargetIndex(hitLaneId, pointerY) : undefined;
|
|
1131
|
+
this.activeSession.currentTargetIndex = targetIndex;
|
|
1132
|
+
if (this.feedback.hoveredLaneId !== hitLaneId || this.feedback.targetIndex !== targetIndex) {
|
|
1133
|
+
this.feedback = {
|
|
1134
|
+
...this.feedback,
|
|
1135
|
+
hoveredLaneId: hitLaneId,
|
|
1136
|
+
targetIndex,
|
|
1137
|
+
};
|
|
1138
|
+
this.notifyListeners();
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export interface DroppingState {
|
|
1144
|
+
readonly operationId: string;
|
|
1145
|
+
readonly taskId: string;
|
|
1146
|
+
readonly sourceLaneId: string;
|
|
1147
|
+
readonly task: KanbanTask | null;
|
|
1148
|
+
readonly projectName: string | null;
|
|
1149
|
+
readonly targetLaneId: string;
|
|
1150
|
+
readonly targetIndex: number;
|
|
1151
|
+
readonly isDrop: boolean;
|
|
1152
|
+
readonly fromX: number;
|
|
1153
|
+
readonly fromY: number;
|
|
1154
|
+
readonly toX: number;
|
|
1155
|
+
readonly toY: number;
|
|
1156
|
+
readonly releaseX?: number;
|
|
1157
|
+
readonly releaseY?: number;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
export interface UseKanbanDragReturn {
|
|
1161
|
+
controller: KanbanDragController;
|
|
1162
|
+
slotState: DragSlotState;
|
|
1163
|
+
feedback: DragFeedback;
|
|
1164
|
+
isDropping: boolean;
|
|
1165
|
+
droppingTaskId: string | null;
|
|
1166
|
+
droppingState: DroppingState | null;
|
|
1167
|
+
commitDrop: () => Promise<void>;
|
|
1168
|
+
abortDrop: () => void;
|
|
1169
|
+
getTaskPreview?: (taskId: string) => TaskPreviewItem | null;
|
|
1170
|
+
setGetTaskPreview?: (fn?: (taskId: string) => TaskPreviewItem | null) => void;
|
|
1171
|
+
bindLane: (laneId: string) => LaneDragBinding;
|
|
1172
|
+
bindContainerRef: (instance: any) => void;
|
|
1173
|
+
handleContainerLayout: (layout: Rect) => void;
|
|
1174
|
+
handleContainerScroll: (scrollX: number) => void;
|
|
1175
|
+
getContainerBounds: () => Rect | null;
|
|
1176
|
+
startGesture: KanbanDragController["startGesture"];
|
|
1177
|
+
moveGesture: KanbanDragController["moveGesture"];
|
|
1178
|
+
releaseGesture: (releaseX?: number, releaseY?: number) => Promise<void>;
|
|
1179
|
+
cancelGesture: () => void;
|
|
1180
|
+
registerContainerBounds: KanbanDragController["registerContainerBounds"];
|
|
1181
|
+
handleScroll: KanbanDragController["handleScroll"];
|
|
1182
|
+
registerLaneLayout: KanbanDragController["registerLaneLayout"];
|
|
1183
|
+
registerCardsViewportLayout: KanbanDragController["registerCardsViewportLayout"];
|
|
1184
|
+
handleLaneScroll: KanbanDragController["handleLaneScroll"];
|
|
1185
|
+
registerCardLayout: KanbanDragController["registerCardLayout"];
|
|
1186
|
+
unregisterCardLayout: KanbanDragController["unregisterCardLayout"];
|
|
1187
|
+
setLaneCardOrder: KanbanDragController["setLaneCardOrder"];
|
|
1188
|
+
isTaskDragging: (taskId: string) => boolean;
|
|
1189
|
+
isLaneHovered: (laneId: string) => boolean;
|
|
1190
|
+
isDragLocked: () => boolean;
|
|
1191
|
+
getLaneLayout: KanbanDragController["getLaneLayout"];
|
|
1192
|
+
getCardsViewportLayout: KanbanDragController["getCardsViewportLayout"];
|
|
1193
|
+
getLaneScroll: KanbanDragController["getLaneScroll"];
|
|
1194
|
+
getDropSlotPosition: KanbanDragController["getDropSlotPosition"];
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export function useKanbanDrag(options: KanbanDragOptions = {}): UseKanbanDragReturn {
|
|
1198
|
+
const { lanes = [], onMoveTask, onReorderTask, getTaskPreview } = options;
|
|
1199
|
+
const controllerRef = useRef<KanbanDragController | null>(null);
|
|
1200
|
+
if (!controllerRef.current) {
|
|
1201
|
+
controllerRef.current = new KanbanDragController({
|
|
1202
|
+
lanes,
|
|
1203
|
+
onMoveTask,
|
|
1204
|
+
onReorderTask,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
const controller = controllerRef.current;
|
|
1208
|
+
|
|
1209
|
+
controller.setLanes(lanes);
|
|
1210
|
+
if (onMoveTask) {
|
|
1211
|
+
controller.setOnMoveTask(onMoveTask);
|
|
1212
|
+
}
|
|
1213
|
+
if (onReorderTask) {
|
|
1214
|
+
controller.setOnReorderTask(onReorderTask);
|
|
1215
|
+
}
|
|
1216
|
+
if (getTaskPreview) {
|
|
1217
|
+
controller.setGetTaskPreview(getTaskPreview);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const slotState = useSyncExternalStore(
|
|
1221
|
+
controller.subscribeSlot,
|
|
1222
|
+
controller.getSlotState,
|
|
1223
|
+
controller.getSlotState
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
const feedback = useSyncExternalStore(
|
|
1227
|
+
controller.subscribe,
|
|
1228
|
+
controller.getFeedback,
|
|
1229
|
+
controller.getFeedback
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
const droppingState = useSyncExternalStore(
|
|
1233
|
+
controller.subscribeDrop,
|
|
1234
|
+
controller.getDroppingState,
|
|
1235
|
+
controller.getDroppingState
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
const containerScrollRef = useRef<any>(null);
|
|
1239
|
+
const currentScrollX = useRef(0);
|
|
1240
|
+
|
|
1241
|
+
// Smooth edge auto-scroll when dragging near viewport boundaries
|
|
1242
|
+
useEffect(() => {
|
|
1243
|
+
const curFeedback = controller.getFeedback();
|
|
1244
|
+
const velocity = curFeedback.autoScrollVelocity ?? 0;
|
|
1245
|
+
if (!velocity || !curFeedback.isDragging) return;
|
|
1246
|
+
|
|
1247
|
+
const timer = setInterval(() => {
|
|
1248
|
+
const liveFeedback = controller.getFeedback();
|
|
1249
|
+
const curVelocity = liveFeedback.autoScrollVelocity ?? 0;
|
|
1250
|
+
if (!curVelocity || !liveFeedback.isDragging) return;
|
|
1251
|
+
const nextOffset = Math.max(0, currentScrollX.current + curVelocity * 14);
|
|
1252
|
+
currentScrollX.current = nextOffset;
|
|
1253
|
+
containerScrollRef.current?.scrollTo({ x: nextOffset, animated: false });
|
|
1254
|
+
}, 16);
|
|
1255
|
+
|
|
1256
|
+
return () => clearInterval(timer);
|
|
1257
|
+
}, [slotState.isDragging, controller]);
|
|
1258
|
+
|
|
1259
|
+
const handleContainerLayout = (layout: Rect) => {
|
|
1260
|
+
controller.handleContainerLayout(layout);
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
const handleContainerScroll = (scrollX: number) => {
|
|
1264
|
+
currentScrollX.current = scrollX;
|
|
1265
|
+
controller.handleScroll(scrollX);
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
const bindContainerRef = (instance: any) => {
|
|
1269
|
+
containerScrollRef.current = instance;
|
|
1270
|
+
controller.bindContainerRef(instance);
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
const commitDrop = async () => {
|
|
1274
|
+
const drop = controller.getDroppingState();
|
|
1275
|
+
if (drop) {
|
|
1276
|
+
await controller.reportDropComplete(drop.operationId);
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
const abortDrop = () => {
|
|
1281
|
+
const drop = controller.getDroppingState();
|
|
1282
|
+
if (drop) {
|
|
1283
|
+
controller.abortDrop(drop.operationId);
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
const handleDragRelease = async (releaseX?: number, releaseY?: number): Promise<void> => {
|
|
1288
|
+
await controller.beginDropAnimation({ x: releaseX, y: releaseY });
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
const handleDragCancel = () => {
|
|
1292
|
+
const drop = controller.getDroppingState();
|
|
1293
|
+
if (drop) {
|
|
1294
|
+
controller.abortDrop(drop.operationId);
|
|
1295
|
+
} else {
|
|
1296
|
+
controller.cancelGesture();
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
controller.setReleaseHandler(handleDragRelease);
|
|
1301
|
+
controller.setCancelHandler(handleDragCancel);
|
|
1302
|
+
|
|
1303
|
+
const isTaskDragging = (taskId: string): boolean => {
|
|
1304
|
+
if (droppingState?.taskId === taskId) return true;
|
|
1305
|
+
return controller.isTaskDragging(taskId);
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
const isLaneHovered = (laneId: string): boolean => {
|
|
1309
|
+
if (droppingState) {
|
|
1310
|
+
return droppingState.targetLaneId === laneId;
|
|
1311
|
+
}
|
|
1312
|
+
return controller.isLaneHovered(laneId);
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
const isDragLocked = (): boolean => {
|
|
1316
|
+
return controller.isDragLocked() || droppingState !== null;
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
const bindLane = (laneId: string): LaneDragBinding => {
|
|
1320
|
+
const isHovered = droppingState
|
|
1321
|
+
? droppingState.targetLaneId === laneId
|
|
1322
|
+
: slotState.isDragging && slotState.hoveredLaneId === laneId;
|
|
1323
|
+
const targetIndex = droppingState
|
|
1324
|
+
? (droppingState.targetLaneId === laneId ? droppingState.targetIndex : -1)
|
|
1325
|
+
: isHovered
|
|
1326
|
+
? slotState.targetIndex
|
|
1327
|
+
: -1;
|
|
1328
|
+
|
|
1329
|
+
return {
|
|
1330
|
+
isHovered,
|
|
1331
|
+
targetIndex,
|
|
1332
|
+
isTaskDragging,
|
|
1333
|
+
draggedCardHeight: slotState.draggedCardHeight,
|
|
1334
|
+
isDragLocked,
|
|
1335
|
+
bindCard: (taskId: string, onSelectTask?: () => void, factory?: PanResponderFactory) =>
|
|
1336
|
+
controller.bindCard(laneId, taskId, onSelectTask, factory),
|
|
1337
|
+
registerLaneLayout: (layout: Rect) => controller.registerLaneLayout(laneId, layout),
|
|
1338
|
+
registerCardsViewport: (layout: Rect) => controller.registerCardsViewportLayout(laneId, layout),
|
|
1339
|
+
handleLaneScroll: (scrollY: number) => controller.handleLaneScroll(laneId, scrollY),
|
|
1340
|
+
registerCardLayout: (taskId: string, layout: Rect) => controller.registerCardLayout(laneId, taskId, layout),
|
|
1341
|
+
unregisterCardLayout: (taskId: string) => controller.unregisterCardLayout(laneId, taskId),
|
|
1342
|
+
setDropSpacerY: (y: number | null) => controller.setDropSpacerY(y),
|
|
1343
|
+
setLaneCardOrder: (laneIdOrTaskIds: string | string[], taskIds?: string[]) => {
|
|
1344
|
+
if (Array.isArray(laneIdOrTaskIds)) {
|
|
1345
|
+
controller.setLaneCardOrder(laneId, laneIdOrTaskIds);
|
|
1346
|
+
} else if (taskIds) {
|
|
1347
|
+
controller.setLaneCardOrder(laneIdOrTaskIds, taskIds);
|
|
1348
|
+
}
|
|
1349
|
+
},
|
|
1350
|
+
startGesture: controller.startGesture,
|
|
1351
|
+
moveGesture: controller.moveGesture,
|
|
1352
|
+
releaseGesture: handleDragRelease,
|
|
1353
|
+
cancelGesture: handleDragCancel,
|
|
1354
|
+
};
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
return {
|
|
1358
|
+
controller,
|
|
1359
|
+
slotState,
|
|
1360
|
+
feedback,
|
|
1361
|
+
isDropping: droppingState !== null,
|
|
1362
|
+
droppingTaskId: droppingState?.taskId ?? null,
|
|
1363
|
+
droppingState,
|
|
1364
|
+
commitDrop,
|
|
1365
|
+
abortDrop,
|
|
1366
|
+
getTaskPreview,
|
|
1367
|
+
setGetTaskPreview: controller.setGetTaskPreview,
|
|
1368
|
+
bindLane,
|
|
1369
|
+
bindContainerRef,
|
|
1370
|
+
handleContainerLayout,
|
|
1371
|
+
handleContainerScroll,
|
|
1372
|
+
getContainerBounds: controller.getContainerBounds,
|
|
1373
|
+
startGesture: controller.startGesture,
|
|
1374
|
+
moveGesture: controller.moveGesture,
|
|
1375
|
+
releaseGesture: handleDragRelease,
|
|
1376
|
+
cancelGesture: handleDragCancel,
|
|
1377
|
+
registerContainerBounds: controller.registerContainerBounds,
|
|
1378
|
+
handleScroll: controller.handleScroll,
|
|
1379
|
+
registerLaneLayout: controller.registerLaneLayout,
|
|
1380
|
+
registerCardsViewportLayout: controller.registerCardsViewportLayout,
|
|
1381
|
+
handleLaneScroll: controller.handleLaneScroll,
|
|
1382
|
+
registerCardLayout: controller.registerCardLayout,
|
|
1383
|
+
unregisterCardLayout: controller.unregisterCardLayout,
|
|
1384
|
+
setLaneCardOrder: controller.setLaneCardOrder,
|
|
1385
|
+
isTaskDragging,
|
|
1386
|
+
isLaneHovered,
|
|
1387
|
+
isDragLocked,
|
|
1388
|
+
getLaneLayout: controller.getLaneLayout,
|
|
1389
|
+
getCardsViewportLayout: controller.getCardsViewportLayout,
|
|
1390
|
+
getLaneScroll: controller.getLaneScroll,
|
|
1391
|
+
getDropSlotPosition: controller.getDropSlotPosition,
|
|
1392
|
+
};
|
|
1393
|
+
}
|