@lotics/ui 2.6.1 → 3.0.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/package.json +1 -15
- package/src/react_native.d.ts +2 -2
- package/src/cell_date.tsx +0 -30
- package/src/cell_date_format.test.ts +0 -32
- package/src/cell_date_format.ts +0 -73
- package/src/cell_number.test.ts +0 -42
- package/src/cell_number.tsx +0 -25
- package/src/cell_number_format.ts +0 -42
- package/src/cell_select.tsx +0 -68
- package/src/cell_text.tsx +0 -45
- package/src/grid/data_grid.tsx +0 -2003
- package/src/grid/data_grid_columns.test.ts +0 -72
- package/src/grid/data_grid_columns.ts +0 -30
- package/src/grid/data_grid_context.ts +0 -119
- package/src/grid/dispatch_safely.ts +0 -39
- package/src/grid/engine.module.css +0 -114
- package/src/grid/engine.tsx +0 -1042
- package/src/grid/helpers.ts +0 -205
- package/src/grid/layout.test.ts +0 -515
- package/src/grid/layout.ts +0 -425
- package/src/grid/recycling.test.ts +0 -236
- package/src/grid/recycling.ts +0 -172
- package/src/grid/row_cell.module.css +0 -105
- package/src/grid/row_cell.tsx +0 -313
- package/src/grid/search_highlight.ts +0 -71
- package/src/grid/select_cell.tsx +0 -58
- package/src/grid/select_group_summary_cell.tsx +0 -76
- package/src/grid/select_header_cell.tsx +0 -32
- package/src/grid/skeleton_row.module.css +0 -34
- package/src/grid/skeleton_row.tsx +0 -20
- package/src/grid/use_grid_groups.ts +0 -311
- package/src/grid/use_scroll_to_cell.ts +0 -135
- package/src/grid/use_virtual_grid.ts +0 -383
- package/src/grid/visibility.test.ts +0 -208
- package/src/grid/visibility.ts +0 -77
- package/src/kanban/constants.ts +0 -18
- package/src/kanban/default_renderers.tsx +0 -160
- package/src/kanban/drag_preview.tsx +0 -157
- package/src/kanban/index.ts +0 -13
- package/src/kanban/insert_card_zone.tsx +0 -135
- package/src/kanban/kanban_board.tsx +0 -635
- package/src/kanban/kanban_card.tsx +0 -321
- package/src/kanban/kanban_column.tsx +0 -499
- package/src/kanban/placeholders.tsx +0 -54
- package/src/kanban/types.ts +0 -116
|
@@ -1,635 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { FlatList, ScrollView, StyleSheet, View } from "react-native";
|
|
3
|
-
import { KanbanColumn } from "./kanban_column";
|
|
4
|
-
import { DragPreview } from "./drag_preview";
|
|
5
|
-
import { ColumnPlaceholder } from "./placeholders";
|
|
6
|
-
import {
|
|
7
|
-
CardMoveResult,
|
|
8
|
-
ColumnMoveResult,
|
|
9
|
-
DragInfo,
|
|
10
|
-
DragPreviewInfo,
|
|
11
|
-
DropTarget,
|
|
12
|
-
KanbanColumn as KanbanColumnType,
|
|
13
|
-
KanbanRenderAddCardPlaceholderProps,
|
|
14
|
-
KanbanRenderInsertCardButtonProps,
|
|
15
|
-
KanbanRenderCardProps,
|
|
16
|
-
KanbanRenderCollapsedColumnProps,
|
|
17
|
-
KanbanRenderColumnHeaderProps,
|
|
18
|
-
} from "./types";
|
|
19
|
-
import {
|
|
20
|
-
AUTO_SCROLL_SPEED,
|
|
21
|
-
AUTO_SCROLL_THRESHOLD,
|
|
22
|
-
COLUMN_CONTENT_PADDING,
|
|
23
|
-
DEFAULT_COLUMN_GAP,
|
|
24
|
-
DEFAULT_COLUMN_WIDTH,
|
|
25
|
-
DEFAULT_ITEM_GAP,
|
|
26
|
-
MINIMIZED_COLUMN_WIDTH,
|
|
27
|
-
} from "./constants";
|
|
28
|
-
import {
|
|
29
|
-
defaultRenderAddCardPlaceholder,
|
|
30
|
-
defaultRenderCollapsedColumn,
|
|
31
|
-
defaultRenderColumnHeader,
|
|
32
|
-
} from "./default_renderers";
|
|
33
|
-
|
|
34
|
-
export interface KanbanBoardProps<T> {
|
|
35
|
-
columns: KanbanColumnType<T>[];
|
|
36
|
-
renderCard: (props: KanbanRenderCardProps<T>) => React.ReactNode;
|
|
37
|
-
renderColumnHeader?: (props: KanbanRenderColumnHeaderProps) => React.ReactNode;
|
|
38
|
-
renderAddCardPlaceholder?: (props: KanbanRenderAddCardPlaceholderProps) => React.ReactNode;
|
|
39
|
-
/** Custom renderer for the insert card button shown between cards on hover */
|
|
40
|
-
renderInsertCardButton?: (props: KanbanRenderInsertCardButtonProps) => React.ReactNode;
|
|
41
|
-
renderCollapsedColumn?: (props: KanbanRenderCollapsedColumnProps) => React.ReactNode;
|
|
42
|
-
onCardMove?: (result: CardMoveResult<T>) => void;
|
|
43
|
-
onColumnMove?: (result: ColumnMoveResult) => void;
|
|
44
|
-
onCardPress?: (cardId: string, item: T, columnKey: string) => void;
|
|
45
|
-
onAddCard?: (columnKey: string) => void;
|
|
46
|
-
/** Called when user clicks the insert button between cards */
|
|
47
|
-
onAddCardAtIndex?: (columnKey: string, index: number) => void;
|
|
48
|
-
columnWidth?: number;
|
|
49
|
-
columnGap?: number;
|
|
50
|
-
/** Height of each card. Number for fixed height, function for variable heights */
|
|
51
|
-
itemHeight: number | ((item: T, index: number) => number);
|
|
52
|
-
/** Gap between items in a column */
|
|
53
|
-
itemGap?: number;
|
|
54
|
-
/** When true, cards can only be dropped into columns (highlights entire column), not between specific cards */
|
|
55
|
-
columnOnlyCardDrop?: boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Whether cards can be dragged. Defaults to whether `onCardMove` is provided —
|
|
58
|
-
* a card with no move handler does not initiate a drag (it can still be
|
|
59
|
-
* pressed via `onCardPress`). Pass `false` for a read-only / action-driven
|
|
60
|
-
* board.
|
|
61
|
-
*/
|
|
62
|
-
cardDraggable?: boolean;
|
|
63
|
-
/** Whether columns can be reordered. Defaults to whether `onColumnMove` is provided. */
|
|
64
|
-
columnDraggable?: boolean;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Column registration data */
|
|
68
|
-
export interface ColumnRegistration {
|
|
69
|
-
element: HTMLElement;
|
|
70
|
-
headerHeight: number;
|
|
71
|
-
/** FlatList ref for scrolling - null when column is minimized */
|
|
72
|
-
listRef: FlatList | null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function KanbanBoard<T>({
|
|
76
|
-
columns,
|
|
77
|
-
renderCard,
|
|
78
|
-
renderColumnHeader = defaultRenderColumnHeader,
|
|
79
|
-
renderAddCardPlaceholder = defaultRenderAddCardPlaceholder,
|
|
80
|
-
renderInsertCardButton,
|
|
81
|
-
renderCollapsedColumn = defaultRenderCollapsedColumn,
|
|
82
|
-
onCardMove,
|
|
83
|
-
onColumnMove,
|
|
84
|
-
onCardPress,
|
|
85
|
-
onAddCard,
|
|
86
|
-
onAddCardAtIndex,
|
|
87
|
-
columnWidth = DEFAULT_COLUMN_WIDTH,
|
|
88
|
-
columnGap = DEFAULT_COLUMN_GAP,
|
|
89
|
-
itemHeight,
|
|
90
|
-
itemGap = DEFAULT_ITEM_GAP,
|
|
91
|
-
columnOnlyCardDrop = false,
|
|
92
|
-
cardDraggable,
|
|
93
|
-
columnDraggable,
|
|
94
|
-
}: KanbanBoardProps<T>) {
|
|
95
|
-
// A card/column is draggable only when there is a handler to receive the
|
|
96
|
-
// move — an explicit prop overrides. This makes a read-only or action-driven
|
|
97
|
-
// board (no move handler) non-draggable without extra wiring.
|
|
98
|
-
const cardsDraggable = cardDraggable ?? onCardMove != null;
|
|
99
|
-
const columnsDraggable = columnDraggable ?? onColumnMove != null;
|
|
100
|
-
|
|
101
|
-
// Helper to get height for an item
|
|
102
|
-
const getHeight = useCallback(
|
|
103
|
-
(item: T, index: number): number => {
|
|
104
|
-
return typeof itemHeight === "function" ? itemHeight(item, index) : itemHeight;
|
|
105
|
-
},
|
|
106
|
-
[itemHeight],
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// === State ===
|
|
110
|
-
const [dragInfo, setDragInfo] = useState<DragInfo | null>(null);
|
|
111
|
-
const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);
|
|
112
|
-
const [dragPreview, setDragPreview] = useState<DragPreviewInfo | null>(null);
|
|
113
|
-
const [minimizedColumns, setMinimizedColumns] = useState<Set<string>>(new Set());
|
|
114
|
-
|
|
115
|
-
const toggleMinimized = useCallback((columnKey: string) => {
|
|
116
|
-
setMinimizedColumns((prev) => {
|
|
117
|
-
const next = new Set(prev);
|
|
118
|
-
if (next.has(columnKey)) {
|
|
119
|
-
next.delete(columnKey);
|
|
120
|
-
} else {
|
|
121
|
-
next.add(columnKey);
|
|
122
|
-
}
|
|
123
|
-
return next;
|
|
124
|
-
});
|
|
125
|
-
}, []);
|
|
126
|
-
|
|
127
|
-
// === Refs ===
|
|
128
|
-
const dragPositionRef = useRef<{ x: number; y: number } | null>(null);
|
|
129
|
-
const dragPreviewUpdateRef = useRef<((x: number, y: number) => void) | null>(null);
|
|
130
|
-
|
|
131
|
-
// Column data - consolidated into single map
|
|
132
|
-
const columnDataRef = useRef<Map<string, ColumnRegistration & { scrollY: number }>>(new Map());
|
|
133
|
-
|
|
134
|
-
// Track which column mouse is currently over during drag
|
|
135
|
-
const hoveredColumnRef = useRef<string | null>(null);
|
|
136
|
-
|
|
137
|
-
// Refs to avoid stale closures
|
|
138
|
-
const dragInfoRef = useRef<DragInfo | null>(null);
|
|
139
|
-
const dropTargetRef = useRef<DropTarget | null>(null);
|
|
140
|
-
const onCardMoveRef = useRef(onCardMove);
|
|
141
|
-
const onColumnMoveRef = useRef(onColumnMove);
|
|
142
|
-
const columnsRef = useRef(columns);
|
|
143
|
-
const getHeightRef = useRef(getHeight);
|
|
144
|
-
const itemGapRef = useRef(itemGap);
|
|
145
|
-
|
|
146
|
-
// Keep refs in sync
|
|
147
|
-
onCardMoveRef.current = onCardMove;
|
|
148
|
-
onColumnMoveRef.current = onColumnMove;
|
|
149
|
-
columnsRef.current = columns;
|
|
150
|
-
getHeightRef.current = getHeight;
|
|
151
|
-
itemGapRef.current = itemGap;
|
|
152
|
-
|
|
153
|
-
const isDragging = dragInfo !== null;
|
|
154
|
-
|
|
155
|
-
// === Drop Target Detection ===
|
|
156
|
-
const findDropTarget = useCallback(
|
|
157
|
-
(clientX: number, clientY: number, dragType: "card" | "column"): DropTarget | null => {
|
|
158
|
-
if (dragType === "column") {
|
|
159
|
-
const draggedColumnKey = dragInfoRef.current?.id;
|
|
160
|
-
const sourceIndex = dragInfoRef.current?.sourceIndex ?? 0;
|
|
161
|
-
|
|
162
|
-
// The dragged column is not rendered (hidden), so we work with visible columns only
|
|
163
|
-
const visibleColumns = columnsRef.current.filter((c) => c.key !== draggedColumnKey);
|
|
164
|
-
|
|
165
|
-
// Sort by visual position (left edge) to handle any layout differences
|
|
166
|
-
const sortedColumns = [...visibleColumns].sort((a, b) => {
|
|
167
|
-
const dataA = columnDataRef.current.get(a.key);
|
|
168
|
-
const dataB = columnDataRef.current.get(b.key);
|
|
169
|
-
if (!dataA || !dataB) return 0;
|
|
170
|
-
return (
|
|
171
|
-
dataA.element.getBoundingClientRect().left - dataB.element.getBoundingClientRect().left
|
|
172
|
-
);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Find drop position among visible columns using midpoint detection
|
|
176
|
-
let visibleTargetIndex = 0;
|
|
177
|
-
for (let i = 0; i < sortedColumns.length; i++) {
|
|
178
|
-
const data = columnDataRef.current.get(sortedColumns[i].key);
|
|
179
|
-
if (!data) continue;
|
|
180
|
-
const rect = data.element.getBoundingClientRect();
|
|
181
|
-
const midpoint = rect.left + rect.width / 2;
|
|
182
|
-
if (clientX < midpoint) {
|
|
183
|
-
visibleTargetIndex = i;
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
visibleTargetIndex = i + 1;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Map visible index back to full array index
|
|
190
|
-
// Example: columns [A, B, C, D], dragging B (sourceIndex=1)
|
|
191
|
-
// Visible: [A, C, D] at indices [0, 1, 2]
|
|
192
|
-
// If visibleTargetIndex=2 (between C and D), actual index should be 3
|
|
193
|
-
// because B was at index 1, so indices after it shifted down by 1
|
|
194
|
-
const targetIndex =
|
|
195
|
-
visibleTargetIndex > sourceIndex ? visibleTargetIndex + 1 : visibleTargetIndex;
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
type: "column",
|
|
199
|
-
columnKey:
|
|
200
|
-
columnsRef.current[Math.min(targetIndex, columnsRef.current.length - 1)]?.key ?? "",
|
|
201
|
-
index: targetIndex,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// For card drag, detect column from pointer position
|
|
206
|
-
// (more reliable than relying on pointerenter events)
|
|
207
|
-
let columnKey: string | null = null;
|
|
208
|
-
let columnData:
|
|
209
|
-
| (typeof columnDataRef.current extends Map<string, infer V> ? V : never)
|
|
210
|
-
| null = null;
|
|
211
|
-
let rect: DOMRect | null = null;
|
|
212
|
-
|
|
213
|
-
// Find which column contains the pointer
|
|
214
|
-
for (const [key, data] of columnDataRef.current) {
|
|
215
|
-
const columnRect = data.element.getBoundingClientRect();
|
|
216
|
-
if (
|
|
217
|
-
clientX >= columnRect.left &&
|
|
218
|
-
clientX <= columnRect.right &&
|
|
219
|
-
clientY >= columnRect.top &&
|
|
220
|
-
clientY <= columnRect.bottom
|
|
221
|
-
) {
|
|
222
|
-
columnKey = key;
|
|
223
|
-
columnData = data;
|
|
224
|
-
rect = columnRect;
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (!columnKey || !columnData || !rect) return null;
|
|
230
|
-
|
|
231
|
-
// Update hoveredColumnRef for auto-scroll
|
|
232
|
-
hoveredColumnRef.current = columnKey;
|
|
233
|
-
|
|
234
|
-
// Column-only card drop mode: just return the column, no specific index
|
|
235
|
-
if (columnOnlyCardDrop) {
|
|
236
|
-
return { type: "card", columnKey, index: -1 };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Y position relative to item layout (subtract content padding)
|
|
240
|
-
const relativeY =
|
|
241
|
-
clientY - rect.top - columnData.headerHeight + columnData.scrollY - COLUMN_CONTENT_PADDING;
|
|
242
|
-
|
|
243
|
-
// Find card index based on cumulative heights
|
|
244
|
-
const column = columnsRef.current.find((c) => c.key === columnKey);
|
|
245
|
-
const items = column?.items ?? [];
|
|
246
|
-
|
|
247
|
-
// Get dragging card info
|
|
248
|
-
const draggedCardSourceColumn = dragInfoRef.current?.sourceColumnKey;
|
|
249
|
-
const draggedCardSourceIndex = dragInfoRef.current?.sourceIndex ?? -1;
|
|
250
|
-
const isInSourceColumn = draggedCardSourceColumn === columnKey;
|
|
251
|
-
|
|
252
|
-
let offset = 0;
|
|
253
|
-
for (let i = 0; i < items.length; i++) {
|
|
254
|
-
const height = getHeightRef.current(items[i].data, i);
|
|
255
|
-
const midpoint = offset + height / 2;
|
|
256
|
-
if (relativeY < midpoint) {
|
|
257
|
-
// When in source column, dropping at sourceIndex or sourceIndex+1
|
|
258
|
-
// both mean "keep card in same position", so normalize to sourceIndex
|
|
259
|
-
if (
|
|
260
|
-
isInSourceColumn &&
|
|
261
|
-
(i === draggedCardSourceIndex || i === draggedCardSourceIndex + 1)
|
|
262
|
-
) {
|
|
263
|
-
return { type: "card", columnKey, index: draggedCardSourceIndex };
|
|
264
|
-
}
|
|
265
|
-
return { type: "card", columnKey, index: i };
|
|
266
|
-
}
|
|
267
|
-
offset += height + itemGapRef.current;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Dropping at the end - same normalization for source column
|
|
271
|
-
if (isInSourceColumn && items.length === draggedCardSourceIndex + 1) {
|
|
272
|
-
return { type: "card", columnKey, index: draggedCardSourceIndex };
|
|
273
|
-
}
|
|
274
|
-
return { type: "card", columnKey, index: items.length };
|
|
275
|
-
},
|
|
276
|
-
[columnOnlyCardDrop],
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
// === Auto-scroll within columns ===
|
|
280
|
-
const performAutoScroll = useCallback((clientX: number, clientY: number) => {
|
|
281
|
-
const columnKey = hoveredColumnRef.current;
|
|
282
|
-
if (!columnKey) return;
|
|
283
|
-
|
|
284
|
-
const columnData = columnDataRef.current.get(columnKey);
|
|
285
|
-
if (!columnData || !columnData.listRef) return; // Skip if minimized (no listRef)
|
|
286
|
-
|
|
287
|
-
const rect = columnData.element.getBoundingClientRect();
|
|
288
|
-
const relativeY = clientY - rect.top - columnData.headerHeight;
|
|
289
|
-
const contentHeight = rect.height - columnData.headerHeight;
|
|
290
|
-
|
|
291
|
-
if (relativeY < AUTO_SCROLL_THRESHOLD) {
|
|
292
|
-
columnData.listRef.scrollToOffset({
|
|
293
|
-
offset: Math.max(0, columnData.scrollY - AUTO_SCROLL_SPEED),
|
|
294
|
-
animated: false,
|
|
295
|
-
});
|
|
296
|
-
} else if (relativeY > contentHeight - AUTO_SCROLL_THRESHOLD) {
|
|
297
|
-
columnData.listRef.scrollToOffset({
|
|
298
|
-
offset: columnData.scrollY + AUTO_SCROLL_SPEED,
|
|
299
|
-
animated: false,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}, []);
|
|
303
|
-
|
|
304
|
-
// === Drag Position Updates ===
|
|
305
|
-
const updateDragPosition = useCallback(
|
|
306
|
-
(clientX: number, clientY: number) => {
|
|
307
|
-
dragPositionRef.current = { x: clientX, y: clientY };
|
|
308
|
-
dragPreviewUpdateRef.current?.(clientX, clientY);
|
|
309
|
-
|
|
310
|
-
const info = dragInfoRef.current;
|
|
311
|
-
if (!info) return;
|
|
312
|
-
|
|
313
|
-
const target = findDropTarget(clientX, clientY, info.type);
|
|
314
|
-
|
|
315
|
-
// Only update state if target changed
|
|
316
|
-
const prev = dropTargetRef.current;
|
|
317
|
-
if (
|
|
318
|
-
target?.type !== prev?.type ||
|
|
319
|
-
target?.columnKey !== prev?.columnKey ||
|
|
320
|
-
target?.index !== prev?.index
|
|
321
|
-
) {
|
|
322
|
-
dropTargetRef.current = target;
|
|
323
|
-
setDropTarget(target);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (info.type === "card") {
|
|
327
|
-
performAutoScroll(clientX, clientY);
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
[findDropTarget, performAutoScroll],
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
// === End Drag ===
|
|
334
|
-
const endDrag = useCallback(() => {
|
|
335
|
-
const info = dragInfoRef.current;
|
|
336
|
-
const target = dropTargetRef.current;
|
|
337
|
-
|
|
338
|
-
if (info && target) {
|
|
339
|
-
if (info.type === "card" && onCardMoveRef.current) {
|
|
340
|
-
const card = columnsRef.current
|
|
341
|
-
.find((col) => col.key === info.sourceColumnKey)
|
|
342
|
-
?.items.find((item) => item.id === info.id);
|
|
343
|
-
|
|
344
|
-
if (card) {
|
|
345
|
-
// Adjust index when moving within same column and moving forward
|
|
346
|
-
// Because removing the card shifts all subsequent indices down by 1
|
|
347
|
-
let targetIndex = target.index;
|
|
348
|
-
if (info.sourceColumnKey === target.columnKey && info.sourceIndex < target.index) {
|
|
349
|
-
targetIndex--;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
onCardMoveRef.current({
|
|
353
|
-
cardId: info.id,
|
|
354
|
-
cardData: card.data,
|
|
355
|
-
sourceColumnKey: info.sourceColumnKey,
|
|
356
|
-
sourceIndex: info.sourceIndex,
|
|
357
|
-
targetColumnKey: target.columnKey,
|
|
358
|
-
targetIndex,
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
} else if (info.type === "column" && onColumnMoveRef.current) {
|
|
362
|
-
// Adjust index when moving forward (same reason as cards)
|
|
363
|
-
// findDropTarget returns "placeholder position", we need "final position after removal"
|
|
364
|
-
let targetIndex = target.index;
|
|
365
|
-
if (info.sourceIndex < target.index) {
|
|
366
|
-
targetIndex--;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
onColumnMoveRef.current({
|
|
370
|
-
columnKey: info.id,
|
|
371
|
-
sourceIndex: info.sourceIndex,
|
|
372
|
-
targetIndex,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Reset all state
|
|
378
|
-
setDragInfo(null);
|
|
379
|
-
setDropTarget(null);
|
|
380
|
-
setDragPreview(null);
|
|
381
|
-
dragInfoRef.current = null;
|
|
382
|
-
dropTargetRef.current = null;
|
|
383
|
-
dragPositionRef.current = null;
|
|
384
|
-
hoveredColumnRef.current = null;
|
|
385
|
-
}, []);
|
|
386
|
-
|
|
387
|
-
// === Global Pointer Listeners ===
|
|
388
|
-
useEffect(() => {
|
|
389
|
-
if (!isDragging) return;
|
|
390
|
-
|
|
391
|
-
const handlePointerMove = (e: PointerEvent) => {
|
|
392
|
-
e.preventDefault();
|
|
393
|
-
updateDragPosition(e.clientX, e.clientY);
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
const handlePointerUp = (e: PointerEvent) => {
|
|
397
|
-
e.preventDefault();
|
|
398
|
-
endDrag();
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const preventDefault = (e: Event) => e.preventDefault();
|
|
402
|
-
|
|
403
|
-
// Use pointer events only - they handle both mouse and touch
|
|
404
|
-
window.addEventListener("pointermove", handlePointerMove);
|
|
405
|
-
window.addEventListener("pointerup", handlePointerUp);
|
|
406
|
-
window.addEventListener("selectstart", preventDefault);
|
|
407
|
-
|
|
408
|
-
return () => {
|
|
409
|
-
window.removeEventListener("pointermove", handlePointerMove);
|
|
410
|
-
window.removeEventListener("pointerup", handlePointerUp);
|
|
411
|
-
window.removeEventListener("selectstart", preventDefault);
|
|
412
|
-
};
|
|
413
|
-
}, [isDragging, updateDragPosition, endDrag]);
|
|
414
|
-
|
|
415
|
-
// === Start Drag Callbacks ===
|
|
416
|
-
const startCardDrag = useCallback(
|
|
417
|
-
(
|
|
418
|
-
cardId: string,
|
|
419
|
-
columnKey: string,
|
|
420
|
-
index: number,
|
|
421
|
-
startX: number,
|
|
422
|
-
startY: number,
|
|
423
|
-
rect: { x: number; y: number; width: number; height: number },
|
|
424
|
-
) => {
|
|
425
|
-
const info: DragInfo = {
|
|
426
|
-
type: "card",
|
|
427
|
-
id: cardId,
|
|
428
|
-
sourceColumnKey: columnKey,
|
|
429
|
-
sourceIndex: index,
|
|
430
|
-
};
|
|
431
|
-
setDragInfo(info);
|
|
432
|
-
dragInfoRef.current = info;
|
|
433
|
-
dragPositionRef.current = { x: startX, y: startY };
|
|
434
|
-
hoveredColumnRef.current = columnKey;
|
|
435
|
-
setDragPreview({
|
|
436
|
-
width: rect.width,
|
|
437
|
-
height: rect.height,
|
|
438
|
-
offsetX: startX - rect.x,
|
|
439
|
-
offsetY: startY - rect.y,
|
|
440
|
-
});
|
|
441
|
-
// In column-only mode, use index -1 to show column highlight instead of position indicator
|
|
442
|
-
const initialIndex = columnOnlyCardDrop ? -1 : index;
|
|
443
|
-
setDropTarget({ type: "card", columnKey, index: initialIndex });
|
|
444
|
-
dropTargetRef.current = { type: "card", columnKey, index: initialIndex };
|
|
445
|
-
},
|
|
446
|
-
[columnOnlyCardDrop],
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
const startColumnDrag = useCallback(
|
|
450
|
-
(
|
|
451
|
-
columnKey: string,
|
|
452
|
-
index: number,
|
|
453
|
-
startX: number,
|
|
454
|
-
startY: number,
|
|
455
|
-
rect: { x: number; y: number; width: number; height: number },
|
|
456
|
-
) => {
|
|
457
|
-
const info: DragInfo = {
|
|
458
|
-
type: "column",
|
|
459
|
-
id: columnKey,
|
|
460
|
-
sourceColumnKey: columnKey,
|
|
461
|
-
sourceIndex: index,
|
|
462
|
-
};
|
|
463
|
-
setDragInfo(info);
|
|
464
|
-
dragInfoRef.current = info;
|
|
465
|
-
dragPositionRef.current = { x: startX, y: startY };
|
|
466
|
-
setDragPreview({
|
|
467
|
-
width: rect.width,
|
|
468
|
-
height: rect.height,
|
|
469
|
-
offsetX: startX - rect.x,
|
|
470
|
-
offsetY: startY - rect.y,
|
|
471
|
-
});
|
|
472
|
-
setDropTarget({ type: "column", columnKey, index });
|
|
473
|
-
dropTargetRef.current = { type: "column", columnKey, index };
|
|
474
|
-
},
|
|
475
|
-
[],
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
// === Column Registration (consolidated) ===
|
|
479
|
-
const registerColumn = useCallback((columnKey: string, data: ColumnRegistration | null) => {
|
|
480
|
-
if (data) {
|
|
481
|
-
const existing = columnDataRef.current.get(columnKey);
|
|
482
|
-
columnDataRef.current.set(columnKey, {
|
|
483
|
-
...data,
|
|
484
|
-
scrollY: existing?.scrollY ?? 0,
|
|
485
|
-
});
|
|
486
|
-
} else {
|
|
487
|
-
columnDataRef.current.delete(columnKey);
|
|
488
|
-
}
|
|
489
|
-
}, []);
|
|
490
|
-
|
|
491
|
-
const updateColumnScroll = useCallback((columnKey: string, scrollY: number) => {
|
|
492
|
-
const data = columnDataRef.current.get(columnKey);
|
|
493
|
-
if (data) {
|
|
494
|
-
data.scrollY = scrollY;
|
|
495
|
-
}
|
|
496
|
-
}, []);
|
|
497
|
-
|
|
498
|
-
// Kept for potential future use, but column detection now happens in findDropTarget
|
|
499
|
-
const handleColumnPointerEnter = useCallback((columnKey: string) => {
|
|
500
|
-
if (dragInfoRef.current?.type === "card") {
|
|
501
|
-
hoveredColumnRef.current = columnKey;
|
|
502
|
-
}
|
|
503
|
-
}, []);
|
|
504
|
-
|
|
505
|
-
// STABLE: Callback for DragPreview position updates
|
|
506
|
-
const handlePositionUpdate = useCallback((cb: (x: number, y: number) => void) => {
|
|
507
|
-
dragPreviewUpdateRef.current = cb;
|
|
508
|
-
}, []);
|
|
509
|
-
|
|
510
|
-
// === Derived State ===
|
|
511
|
-
const dragType = dragInfo?.type ?? null;
|
|
512
|
-
const dragId = dragInfo?.id ?? null;
|
|
513
|
-
const sourceColumnKey = dragInfo?.sourceColumnKey ?? null;
|
|
514
|
-
const dropTargetType = dropTarget?.type ?? null;
|
|
515
|
-
const dropTargetColumnKey = dropTarget?.columnKey ?? null;
|
|
516
|
-
const dropTargetIndex = dropTarget?.index ?? null;
|
|
517
|
-
|
|
518
|
-
// Width of the column being dragged (for placeholder sizing)
|
|
519
|
-
const draggedColumnWidth =
|
|
520
|
-
dragType === "column" && dragId
|
|
521
|
-
? minimizedColumns.has(dragId)
|
|
522
|
-
? MINIMIZED_COLUMN_WIDTH
|
|
523
|
-
: columnWidth
|
|
524
|
-
: columnWidth;
|
|
525
|
-
|
|
526
|
-
// Height of the column being dragged (for placeholder sizing)
|
|
527
|
-
const draggedColumnHeight = dragType === "column" && dragPreview ? dragPreview.height : undefined;
|
|
528
|
-
|
|
529
|
-
// Height of the card being dragged (for placeholder sizing)
|
|
530
|
-
const draggedCardHeight = dragType === "card" && dragPreview ? dragPreview.height : null;
|
|
531
|
-
|
|
532
|
-
// Stable scroll content style
|
|
533
|
-
const scrollContentStyle = useMemo(() => [styles.scrollContent, { gap: columnGap }], [columnGap]);
|
|
534
|
-
|
|
535
|
-
return (
|
|
536
|
-
<View style={styles.container}>
|
|
537
|
-
<ScrollView
|
|
538
|
-
horizontal
|
|
539
|
-
showsHorizontalScrollIndicator={false}
|
|
540
|
-
style={styles.scrollView}
|
|
541
|
-
contentContainerStyle={scrollContentStyle}
|
|
542
|
-
scrollEventThrottle={16}
|
|
543
|
-
>
|
|
544
|
-
{columns.map((column, columnIndex) => {
|
|
545
|
-
const isColumnDragging = dragType === "column" && dragId === column.key;
|
|
546
|
-
const isColumnDropTarget =
|
|
547
|
-
dragType === "column" && dropTargetType === "column" && dropTargetIndex === columnIndex;
|
|
548
|
-
|
|
549
|
-
// Only source column needs cardDragId (to mark dragged card)
|
|
550
|
-
const isSourceColumn = sourceColumnKey === column.key;
|
|
551
|
-
// Only target column needs drop index (to show placeholder)
|
|
552
|
-
const isTargetColumn =
|
|
553
|
-
dragType === "card" && dropTargetType === "card" && dropTargetColumnKey === column.key;
|
|
554
|
-
|
|
555
|
-
const isMinimized = minimizedColumns.has(column.key);
|
|
556
|
-
const effectiveWidth = isMinimized ? MINIMIZED_COLUMN_WIDTH : columnWidth;
|
|
557
|
-
|
|
558
|
-
// In column-only mode, show full column highlight instead of position indicators
|
|
559
|
-
const showCardDropHighlight = columnOnlyCardDrop && isTargetColumn;
|
|
560
|
-
|
|
561
|
-
return (
|
|
562
|
-
<KanbanColumn
|
|
563
|
-
key={column.key}
|
|
564
|
-
columnKey={column.key}
|
|
565
|
-
title={column.title}
|
|
566
|
-
index={columnIndex}
|
|
567
|
-
items={column.items}
|
|
568
|
-
renderCard={renderCard}
|
|
569
|
-
renderColumnHeader={renderColumnHeader}
|
|
570
|
-
renderAddCardPlaceholder={renderAddCardPlaceholder}
|
|
571
|
-
renderInsertCardButton={renderInsertCardButton}
|
|
572
|
-
renderCollapsedColumn={renderCollapsedColumn}
|
|
573
|
-
isDragging={isColumnDragging}
|
|
574
|
-
isDropTarget={isColumnDropTarget}
|
|
575
|
-
isDragInProgress={isDragging}
|
|
576
|
-
// Only pass to source column - other columns don't need to re-render
|
|
577
|
-
cardDragId={isSourceColumn ? dragId : null}
|
|
578
|
-
// Only pass to target column - other columns don't need to re-render
|
|
579
|
-
// Don't pass index in column-only mode (no position indicators)
|
|
580
|
-
cardDropTargetIndex={isTargetColumn && !columnOnlyCardDrop ? dropTargetIndex : null}
|
|
581
|
-
draggedCardHeight={isTargetColumn ? draggedCardHeight : null}
|
|
582
|
-
showCardDropHighlight={showCardDropHighlight}
|
|
583
|
-
columnWidth={effectiveWidth}
|
|
584
|
-
placeholderWidth={isColumnDropTarget ? draggedColumnWidth : effectiveWidth}
|
|
585
|
-
placeholderHeight={isColumnDropTarget ? draggedColumnHeight : undefined}
|
|
586
|
-
itemHeight={itemHeight}
|
|
587
|
-
itemGap={itemGap}
|
|
588
|
-
onCardPress={onCardPress}
|
|
589
|
-
onAddCard={onAddCard}
|
|
590
|
-
onAddCardAtIndex={onAddCardAtIndex}
|
|
591
|
-
cardDraggable={cardsDraggable}
|
|
592
|
-
columnDraggable={columnsDraggable}
|
|
593
|
-
startCardDrag={startCardDrag}
|
|
594
|
-
startColumnDrag={startColumnDrag}
|
|
595
|
-
registerColumn={registerColumn}
|
|
596
|
-
updateColumnScroll={updateColumnScroll}
|
|
597
|
-
onPointerEnter={handleColumnPointerEnter}
|
|
598
|
-
isMinimized={isMinimized}
|
|
599
|
-
onMinimizeToggle={toggleMinimized}
|
|
600
|
-
/>
|
|
601
|
-
);
|
|
602
|
-
})}
|
|
603
|
-
{dragType === "column" &&
|
|
604
|
-
dropTargetType === "column" &&
|
|
605
|
-
dropTargetIndex === columns.length && (
|
|
606
|
-
<ColumnPlaceholder width={draggedColumnWidth} height={draggedColumnHeight} />
|
|
607
|
-
)}
|
|
608
|
-
</ScrollView>
|
|
609
|
-
<DragPreview
|
|
610
|
-
columns={columns}
|
|
611
|
-
dragInfo={dragInfo}
|
|
612
|
-
dragPreview={dragPreview}
|
|
613
|
-
renderCard={renderCard}
|
|
614
|
-
renderColumnHeader={renderColumnHeader}
|
|
615
|
-
onPositionUpdate={handlePositionUpdate}
|
|
616
|
-
initialPosition={dragPositionRef.current}
|
|
617
|
-
/>
|
|
618
|
-
</View>
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const styles = StyleSheet.create({
|
|
623
|
-
container: {
|
|
624
|
-
flex: 1,
|
|
625
|
-
flexBasis: 0,
|
|
626
|
-
},
|
|
627
|
-
scrollView: {
|
|
628
|
-
flex: 1,
|
|
629
|
-
},
|
|
630
|
-
scrollContent: {
|
|
631
|
-
flexDirection: "row",
|
|
632
|
-
alignItems: "flex-start",
|
|
633
|
-
height: "100%",
|
|
634
|
-
},
|
|
635
|
-
});
|