@flux-ui/components 3.0.0-next.57 → 3.0.0-next.58
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/dist/component/FluxKanban.vue.d.ts +17 -5
- package/dist/component/FluxKanbanCard.vue.d.ts +1 -0
- package/dist/component/FluxKanbanColumn.vue.d.ts +13 -2
- package/dist/composable/private/useAsyncFilterOptions.d.ts +1 -1
- package/dist/composable/private/useKanban.d.ts +13 -3
- package/dist/data/di.d.ts +29 -0
- package/dist/index.css +152 -32
- package/dist/index.js +802 -177
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/component/FluxKanban.vue +83 -5
- package/src/component/FluxKanbanCard.vue +107 -13
- package/src/component/FluxKanbanColumn.vue +200 -13
- package/src/composable/private/useAsyncFilterOptions.ts +8 -14
- package/src/composable/private/useFilterOption.ts +1 -1
- package/src/composable/private/useKanban.ts +593 -13
- package/src/css/component/FluxKanban.module.scss +205 -0
- package/src/data/di.ts +33 -0
- package/src/data/filter.ts +1 -1
- package/src/css/component/FluxKanbanCard.module.scss +0 -35
- package/src/css/component/FluxKanbanColumn.module.scss +0 -49
|
@@ -1,20 +1,149 @@
|
|
|
1
|
-
import type { FluxKanbanMoveEvent } from '@flux-ui/types';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import type { FluxKanbanMoveColumnEvent, FluxKanbanMoveEvent } from '@flux-ui/types';
|
|
2
|
+
import type { Ref } from 'vue';
|
|
3
|
+
import { computed, ref, unref } from 'vue';
|
|
4
|
+
import type { FluxKanbanColumnDragState, FluxKanbanDragState, FluxKanbanInjection, FluxKanbanKeyboardDirection } from '$flux/data/di';
|
|
5
|
+
|
|
6
|
+
export type UseKanbanOptions = {
|
|
7
|
+
readonly disabled: Ref<boolean>;
|
|
8
|
+
readonly reorderableColumns: Ref<boolean>;
|
|
9
|
+
readonly canMove?: Ref<((event: FluxKanbanMoveEvent) => boolean) | undefined>;
|
|
10
|
+
readonly onMove: (event: FluxKanbanMoveEvent) => void;
|
|
11
|
+
readonly onMoveColumn: (event: FluxKanbanMoveColumnEvent) => void;
|
|
12
|
+
readonly onAnnounce: (message: string) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const AUTOSCROLL_ZONE = 40;
|
|
16
|
+
const AUTOSCROLL_MAX_SPEED = 12;
|
|
17
|
+
const DRAG_LEAVE_GRACE_MS = 50;
|
|
4
18
|
|
|
5
19
|
/**
|
|
6
20
|
* Internal composable for managing kanban drag-and-drop state.
|
|
7
|
-
* Provides card registration, drag tracking,
|
|
21
|
+
* Provides card registration, drag tracking, drop target management,
|
|
22
|
+
* keyboard drag-and-drop, column reordering, drop validation and auto-scroll.
|
|
8
23
|
*/
|
|
9
|
-
export function useKanban(
|
|
24
|
+
export function useKanban(options: UseKanbanOptions): FluxKanbanInjection {
|
|
10
25
|
const dragState = ref<FluxKanbanDragState | null>(null);
|
|
26
|
+
const columnDragState = ref<FluxKanbanColumnDragState | null>(null);
|
|
27
|
+
|
|
11
28
|
const cardRegistry = new WeakMap<Element, { readonly cardId: string | number }>();
|
|
29
|
+
const cardElementsById = new Map<string | number, Element>();
|
|
30
|
+
const columnRegistry = new WeakMap<Element, { readonly columnId: string | number }>();
|
|
31
|
+
const columnElementsById = new Map<string | number, Element>();
|
|
32
|
+
const columnBodyById = new Map<string | number, Element>();
|
|
33
|
+
|
|
34
|
+
let boardElement: Element | null = null;
|
|
35
|
+
let clearTimer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
let autoScrollFrame: number | null = null;
|
|
37
|
+
let autoScrollX = 0;
|
|
38
|
+
let autoScrollY = 0;
|
|
39
|
+
let autoScrollVerticalTarget: Element | null = null;
|
|
40
|
+
|
|
41
|
+
const isDropAllowed = computed(() => {
|
|
42
|
+
const state = unref(dragState);
|
|
43
|
+
|
|
44
|
+
if (!state || state.dropColumnId === null) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const validate = unref(options.canMove);
|
|
49
|
+
|
|
50
|
+
if (!validate) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return validate(buildMoveEvent(state));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function buildMoveEvent(state: FluxKanbanDragState): FluxKanbanMoveEvent {
|
|
58
|
+
return {
|
|
59
|
+
cardId: state.cardId,
|
|
60
|
+
fromColumnId: state.fromColumnId,
|
|
61
|
+
toColumnId: state.dropColumnId as string | number,
|
|
62
|
+
beforeCardId: state.beforeCardId ?? undefined
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clearTimerIfAny(): void {
|
|
67
|
+
if (clearTimer !== null) {
|
|
68
|
+
clearTimeout(clearTimer);
|
|
69
|
+
clearTimer = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isSelfDrop(state: FluxKanbanDragState): boolean {
|
|
74
|
+
if (state.dropColumnId !== state.fromColumnId) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (state.beforeCardId === state.cardId) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fromBody = columnBodyById.get(state.fromColumnId);
|
|
83
|
+
|
|
84
|
+
if (!fromBody) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const cards = Array.from(fromBody.children).filter(child => cardRegistry.has(child));
|
|
89
|
+
const draggedIndex = cards.findIndex(elm => cardRegistry.get(elm)?.cardId === state.cardId);
|
|
90
|
+
|
|
91
|
+
if (draggedIndex === -1) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (state.beforeCardId === null) {
|
|
96
|
+
return draggedIndex === cards.length - 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const beforeIndex = cards.findIndex(elm => cardRegistry.get(elm)?.cardId === state.beforeCardId);
|
|
100
|
+
return beforeIndex !== -1 && beforeIndex === draggedIndex + 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getColumnIndex(columnId: string | number): number {
|
|
104
|
+
if (!boardElement) {
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const columns = Array.from(boardElement.children).filter(child => columnRegistry.has(child));
|
|
109
|
+
return columns.findIndex(elm => columnRegistry.get(elm)?.columnId === columnId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getColumnByIndex(index: number): { readonly columnId: string | number } | null {
|
|
113
|
+
if (!boardElement) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const columns = Array.from(boardElement.children).filter(child => columnRegistry.has(child));
|
|
118
|
+
const elm = columns[index];
|
|
119
|
+
|
|
120
|
+
return elm ? columnRegistry.get(elm)! : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getCardsInColumn(columnId: string | number): (string | number)[] {
|
|
124
|
+
const body = columnBodyById.get(columnId);
|
|
125
|
+
|
|
126
|
+
if (!body) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Array.from(body.children)
|
|
131
|
+
.map(child => cardRegistry.get(child)?.cardId)
|
|
132
|
+
.filter((id): id is string | number => id !== undefined);
|
|
133
|
+
}
|
|
12
134
|
|
|
13
135
|
function registerCard(element: Element, cardId: string | number): void {
|
|
14
136
|
cardRegistry.set(element, {cardId});
|
|
137
|
+
cardElementsById.set(cardId, element);
|
|
15
138
|
}
|
|
16
139
|
|
|
17
140
|
function unregisterCard(element: Element): void {
|
|
141
|
+
const info = cardRegistry.get(element);
|
|
142
|
+
|
|
143
|
+
if (info) {
|
|
144
|
+
cardElementsById.delete(info.cardId);
|
|
145
|
+
}
|
|
146
|
+
|
|
18
147
|
cardRegistry.delete(element);
|
|
19
148
|
}
|
|
20
149
|
|
|
@@ -22,11 +151,50 @@ export function useKanban(onMove: (event: FluxKanbanMoveEvent) => void): FluxKan
|
|
|
22
151
|
return cardRegistry.get(element);
|
|
23
152
|
}
|
|
24
153
|
|
|
154
|
+
function registerColumn(element: Element, columnId: string | number): void {
|
|
155
|
+
columnRegistry.set(element, {columnId});
|
|
156
|
+
columnElementsById.set(columnId, element);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function unregisterColumn(element: Element): void {
|
|
160
|
+
const info = columnRegistry.get(element);
|
|
161
|
+
|
|
162
|
+
if (info) {
|
|
163
|
+
columnElementsById.delete(info.columnId);
|
|
164
|
+
columnBodyById.delete(info.columnId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
columnRegistry.delete(element);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getColumnInfo(element: Element): { readonly columnId: string | number } | undefined {
|
|
171
|
+
return columnRegistry.get(element);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function setBoardElement(element: Element | null): void {
|
|
175
|
+
boardElement = element;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function setColumnBodyElement(columnId: string | number, element: Element | null): void {
|
|
179
|
+
if (element) {
|
|
180
|
+
columnBodyById.set(columnId, element);
|
|
181
|
+
} else {
|
|
182
|
+
columnBodyById.delete(columnId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
25
186
|
function startDrag(cardId: string | number, fromColumnId: string | number): void {
|
|
26
|
-
|
|
187
|
+
if (unref(options.disabled)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
clearTimerIfAny();
|
|
192
|
+
dragState.value = {mode: 'pointer', cardId, fromColumnId, dropColumnId: null, beforeCardId: null};
|
|
27
193
|
}
|
|
28
194
|
|
|
29
195
|
function endDrag(): void {
|
|
196
|
+
clearTimerIfAny();
|
|
197
|
+
stopAutoScroll();
|
|
30
198
|
dragState.value = null;
|
|
31
199
|
}
|
|
32
200
|
|
|
@@ -35,6 +203,7 @@ export function useKanban(onMove: (event: FluxKanbanMoveEvent) => void): FluxKan
|
|
|
35
203
|
return;
|
|
36
204
|
}
|
|
37
205
|
|
|
206
|
+
clearTimerIfAny();
|
|
38
207
|
dragState.value = {...dragState.value, dropColumnId: columnId, beforeCardId};
|
|
39
208
|
}
|
|
40
209
|
|
|
@@ -43,26 +212,437 @@ export function useKanban(onMove: (event: FluxKanbanMoveEvent) => void): FluxKan
|
|
|
43
212
|
return;
|
|
44
213
|
}
|
|
45
214
|
|
|
46
|
-
|
|
215
|
+
clearTimerIfAny();
|
|
216
|
+
clearTimer = setTimeout(() => {
|
|
217
|
+
clearTimer = null;
|
|
218
|
+
|
|
219
|
+
if (!dragState.value) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
dragState.value = {...dragState.value, dropColumnId: null, beforeCardId: null};
|
|
224
|
+
}, DRAG_LEAVE_GRACE_MS);
|
|
47
225
|
}
|
|
48
226
|
|
|
49
227
|
function commitDrop(): void {
|
|
50
228
|
const state = dragState.value;
|
|
229
|
+
clearTimerIfAny();
|
|
230
|
+
stopAutoScroll();
|
|
51
231
|
|
|
52
232
|
if (!state || state.dropColumnId === null) {
|
|
53
233
|
dragState.value = null;
|
|
54
234
|
return;
|
|
55
235
|
}
|
|
56
236
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
237
|
+
if (isSelfDrop(state)) {
|
|
238
|
+
dragState.value = null;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const event = buildMoveEvent(state);
|
|
243
|
+
const validate = unref(options.canMove);
|
|
244
|
+
|
|
245
|
+
if (validate && !validate(event)) {
|
|
246
|
+
options.onAnnounce('Drop not allowed.');
|
|
247
|
+
dragState.value = null;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
options.onMove(event);
|
|
252
|
+
options.onAnnounce(`Card moved to ${String(state.dropColumnId)}.`);
|
|
253
|
+
dragState.value = null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* region keyboard */
|
|
257
|
+
|
|
258
|
+
function findCurrentColumnId(cardId: string | number): string | number | null {
|
|
259
|
+
for (const columnId of columnElementsById.keys()) {
|
|
260
|
+
if (getCardsInColumn(columnId).includes(cardId)) {
|
|
261
|
+
return columnId;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function findCurrentBeforeCardId(cardId: string | number, columnId: string | number): string | number | null {
|
|
269
|
+
const cards = getCardsInColumn(columnId);
|
|
270
|
+
const idx = cards.indexOf(cardId);
|
|
271
|
+
|
|
272
|
+
if (idx === -1) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return cards[idx + 1] ?? null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function grabCard(cardId: string | number, fromColumnId: string | number): void {
|
|
280
|
+
if (unref(options.disabled)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
clearTimerIfAny();
|
|
285
|
+
|
|
286
|
+
dragState.value = {
|
|
287
|
+
mode: 'keyboard',
|
|
288
|
+
cardId,
|
|
289
|
+
fromColumnId,
|
|
290
|
+
dropColumnId: null,
|
|
291
|
+
beforeCardId: null,
|
|
292
|
+
originBeforeCardId: findCurrentBeforeCardId(cardId, fromColumnId)
|
|
293
|
+
};
|
|
294
|
+
options.onAnnounce('Card grabbed. Use arrow keys to move, Enter to drop, Escape to cancel.');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function moveKeyboard(direction: FluxKanbanKeyboardDirection): void {
|
|
298
|
+
const state = dragState.value;
|
|
299
|
+
|
|
300
|
+
if (!state || state.mode !== 'keyboard') {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const cardId = state.cardId;
|
|
305
|
+
const currentColumnId = findCurrentColumnId(cardId);
|
|
306
|
+
|
|
307
|
+
if (currentColumnId === null) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const target = direction === 'up' || direction === 'down'
|
|
312
|
+
? computeWithinColumnTarget(cardId, currentColumnId, direction)
|
|
313
|
+
: computeAcrossColumnTarget(currentColumnId, direction);
|
|
314
|
+
|
|
315
|
+
if (!target) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const event: FluxKanbanMoveEvent = {
|
|
320
|
+
cardId,
|
|
321
|
+
fromColumnId: currentColumnId,
|
|
322
|
+
toColumnId: target.columnId,
|
|
323
|
+
beforeCardId: target.beforeCardId ?? undefined
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const validate = unref(options.canMove);
|
|
327
|
+
|
|
328
|
+
if (validate && !validate(event)) {
|
|
329
|
+
options.onAnnounce('Move not allowed.');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
options.onMove(event);
|
|
334
|
+
options.onAnnounce(target.announcement);
|
|
335
|
+
restoreCardFocus(cardId);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function restoreCardFocus(cardId: string | number): void {
|
|
339
|
+
requestAnimationFrame(() => {
|
|
340
|
+
const elm = cardElementsById.get(cardId);
|
|
341
|
+
|
|
342
|
+
if (elm instanceof HTMLElement) {
|
|
343
|
+
elm.focus();
|
|
344
|
+
}
|
|
62
345
|
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function computeWithinColumnTarget(
|
|
349
|
+
cardId: string | number,
|
|
350
|
+
columnId: string | number,
|
|
351
|
+
direction: 'up' | 'down'
|
|
352
|
+
): { columnId: string | number; beforeCardId: string | number | null; announcement: string } | null {
|
|
353
|
+
const cards = getCardsInColumn(columnId);
|
|
354
|
+
const idx = cards.indexOf(cardId);
|
|
355
|
+
|
|
356
|
+
if (idx === -1) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (direction === 'up') {
|
|
361
|
+
if (idx === 0) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
columnId,
|
|
367
|
+
beforeCardId: cards[idx - 1],
|
|
368
|
+
announcement: `Position ${idx} of ${cards.length}.`
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (idx === cards.length - 1) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
columnId,
|
|
378
|
+
beforeCardId: cards[idx + 2] ?? null,
|
|
379
|
+
announcement: `Position ${idx + 2} of ${cards.length}.`
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function computeAcrossColumnTarget(
|
|
384
|
+
currentColumnId: string | number,
|
|
385
|
+
direction: 'left' | 'right'
|
|
386
|
+
): { columnId: string | number; beforeCardId: string | number | null; announcement: string } | null {
|
|
387
|
+
const currentIdx = getColumnIndex(currentColumnId);
|
|
388
|
+
|
|
389
|
+
if (currentIdx === -1) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const nextIdx = direction === 'left' ? currentIdx - 1 : currentIdx + 1;
|
|
394
|
+
const nextColumn = getColumnByIndex(nextIdx);
|
|
395
|
+
|
|
396
|
+
if (!nextColumn) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const targetCards = getCardsInColumn(nextColumn.columnId);
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
columnId: nextColumn.columnId,
|
|
404
|
+
beforeCardId: targetCards[0] ?? null,
|
|
405
|
+
announcement: `Moved to column ${String(nextColumn.columnId)}.`
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function commitKeyboardDrop(): void {
|
|
410
|
+
const state = dragState.value;
|
|
411
|
+
|
|
412
|
+
if (!state || state.mode !== 'keyboard') {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
63
415
|
|
|
416
|
+
const currentColumnId = findCurrentColumnId(state.cardId);
|
|
64
417
|
dragState.value = null;
|
|
418
|
+
|
|
419
|
+
if (currentColumnId !== null) {
|
|
420
|
+
options.onAnnounce(`Card dropped in ${String(currentColumnId)}.`);
|
|
421
|
+
}
|
|
65
422
|
}
|
|
66
423
|
|
|
67
|
-
|
|
424
|
+
function cancelKeyboardDrop(): void {
|
|
425
|
+
const state = dragState.value;
|
|
426
|
+
|
|
427
|
+
if (!state || state.mode !== 'keyboard') {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const cardId = state.cardId;
|
|
432
|
+
const currentColumnId = findCurrentColumnId(cardId);
|
|
433
|
+
const currentBeforeCardId = currentColumnId !== null
|
|
434
|
+
? findCurrentBeforeCardId(cardId, currentColumnId)
|
|
435
|
+
: null;
|
|
436
|
+
|
|
437
|
+
const isAtOrigin = currentColumnId === state.fromColumnId
|
|
438
|
+
&& currentBeforeCardId === (state.originBeforeCardId ?? null);
|
|
439
|
+
|
|
440
|
+
if (!isAtOrigin && currentColumnId !== null) {
|
|
441
|
+
options.onMove({
|
|
442
|
+
cardId,
|
|
443
|
+
fromColumnId: currentColumnId,
|
|
444
|
+
toColumnId: state.fromColumnId,
|
|
445
|
+
beforeCardId: state.originBeforeCardId ?? undefined
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
dragState.value = null;
|
|
450
|
+
options.onAnnounce('Drop cancelled.');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isCardGrabbed(cardId: string | number): boolean {
|
|
454
|
+
const state = unref(dragState);
|
|
455
|
+
return state !== null && state.mode === 'keyboard' && state.cardId === cardId;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* endregion */
|
|
459
|
+
|
|
460
|
+
/* region columns */
|
|
461
|
+
|
|
462
|
+
function startColumnDrag(columnId: string | number): void {
|
|
463
|
+
if (!unref(options.reorderableColumns) || unref(options.disabled)) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
columnDragState.value = {columnId, dropBeforeColumnId: null};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function endColumnDrag(): void {
|
|
471
|
+
stopAutoScroll();
|
|
472
|
+
columnDragState.value = null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function updateColumnDropTarget(beforeColumnId: string | number | null): void {
|
|
476
|
+
if (!columnDragState.value) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
columnDragState.value = {...columnDragState.value, dropBeforeColumnId: beforeColumnId};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function commitColumnDrop(): void {
|
|
484
|
+
const state = columnDragState.value;
|
|
485
|
+
stopAutoScroll();
|
|
486
|
+
|
|
487
|
+
if (!state) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (state.dropBeforeColumnId === state.columnId) {
|
|
492
|
+
columnDragState.value = null;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const fromIdx = getColumnIndex(state.columnId);
|
|
497
|
+
const beforeIdx = state.dropBeforeColumnId === null
|
|
498
|
+
? -1
|
|
499
|
+
: getColumnIndex(state.dropBeforeColumnId);
|
|
500
|
+
|
|
501
|
+
if (fromIdx !== -1 && beforeIdx === fromIdx + 1) {
|
|
502
|
+
columnDragState.value = null;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
options.onMoveColumn({
|
|
507
|
+
columnId: state.columnId,
|
|
508
|
+
beforeColumnId: state.dropBeforeColumnId ?? undefined
|
|
509
|
+
});
|
|
510
|
+
columnDragState.value = null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* endregion */
|
|
514
|
+
|
|
515
|
+
/* region auto-scroll */
|
|
516
|
+
|
|
517
|
+
function onPointerMove(clientX: number, clientY: number): void {
|
|
518
|
+
if (!unref(dragState) && !unref(columnDragState)) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
autoScrollX = computeHorizontalScrollDelta(clientX);
|
|
523
|
+
autoScrollY = 0;
|
|
524
|
+
autoScrollVerticalTarget = null;
|
|
525
|
+
|
|
526
|
+
const state = unref(dragState);
|
|
527
|
+
|
|
528
|
+
if (state && state.dropColumnId !== null) {
|
|
529
|
+
const body = columnBodyById.get(state.dropColumnId);
|
|
530
|
+
|
|
531
|
+
if (body) {
|
|
532
|
+
autoScrollY = computeVerticalScrollDelta(body, clientY);
|
|
533
|
+
autoScrollVerticalTarget = body;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (autoScrollX !== 0 || autoScrollY !== 0) {
|
|
538
|
+
startAutoScroll();
|
|
539
|
+
} else {
|
|
540
|
+
stopAutoScroll();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function computeHorizontalScrollDelta(clientX: number): number {
|
|
545
|
+
if (!boardElement) {
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const rect = boardElement.getBoundingClientRect();
|
|
550
|
+
|
|
551
|
+
if (clientX < rect.left + AUTOSCROLL_ZONE) {
|
|
552
|
+
const distance = Math.max(0, clientX - rect.left);
|
|
553
|
+
return -Math.round(((AUTOSCROLL_ZONE - distance) / AUTOSCROLL_ZONE) * AUTOSCROLL_MAX_SPEED);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (clientX > rect.right - AUTOSCROLL_ZONE) {
|
|
557
|
+
const distance = Math.max(0, rect.right - clientX);
|
|
558
|
+
return Math.round(((AUTOSCROLL_ZONE - distance) / AUTOSCROLL_ZONE) * AUTOSCROLL_MAX_SPEED);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function computeVerticalScrollDelta(target: Element, clientY: number): number {
|
|
565
|
+
const rect = target.getBoundingClientRect();
|
|
566
|
+
|
|
567
|
+
if (clientY < rect.top + AUTOSCROLL_ZONE) {
|
|
568
|
+
const distance = Math.max(0, clientY - rect.top);
|
|
569
|
+
return -Math.round(((AUTOSCROLL_ZONE - distance) / AUTOSCROLL_ZONE) * AUTOSCROLL_MAX_SPEED);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (clientY > rect.bottom - AUTOSCROLL_ZONE) {
|
|
573
|
+
const distance = Math.max(0, rect.bottom - clientY);
|
|
574
|
+
return Math.round(((AUTOSCROLL_ZONE - distance) / AUTOSCROLL_ZONE) * AUTOSCROLL_MAX_SPEED);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function startAutoScroll(): void {
|
|
581
|
+
if (autoScrollFrame !== null) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const tick = () => {
|
|
586
|
+
if (autoScrollX !== 0 && boardElement) {
|
|
587
|
+
boardElement.scrollLeft += autoScrollX;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (autoScrollY !== 0 && autoScrollVerticalTarget) {
|
|
591
|
+
autoScrollVerticalTarget.scrollTop += autoScrollY;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (autoScrollX === 0 && autoScrollY === 0) {
|
|
595
|
+
autoScrollFrame = null;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
autoScrollFrame = requestAnimationFrame(tick);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
autoScrollFrame = requestAnimationFrame(tick);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function stopAutoScroll(): void {
|
|
606
|
+
if (autoScrollFrame !== null) {
|
|
607
|
+
cancelAnimationFrame(autoScrollFrame);
|
|
608
|
+
autoScrollFrame = null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
autoScrollX = 0;
|
|
612
|
+
autoScrollY = 0;
|
|
613
|
+
autoScrollVerticalTarget = null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/* endregion */
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
disabled: options.disabled,
|
|
620
|
+
reorderableColumns: options.reorderableColumns,
|
|
621
|
+
dragState,
|
|
622
|
+
columnDragState,
|
|
623
|
+
isDropAllowed,
|
|
624
|
+
registerCard,
|
|
625
|
+
unregisterCard,
|
|
626
|
+
getCardInfo,
|
|
627
|
+
registerColumn,
|
|
628
|
+
unregisterColumn,
|
|
629
|
+
getColumnInfo,
|
|
630
|
+
setBoardElement,
|
|
631
|
+
setColumnBodyElement,
|
|
632
|
+
startDrag,
|
|
633
|
+
endDrag,
|
|
634
|
+
updateDropTarget,
|
|
635
|
+
clearDropTarget,
|
|
636
|
+
commitDrop,
|
|
637
|
+
grabCard,
|
|
638
|
+
moveKeyboard,
|
|
639
|
+
commitKeyboardDrop,
|
|
640
|
+
cancelKeyboardDrop,
|
|
641
|
+
isCardGrabbed,
|
|
642
|
+
startColumnDrag,
|
|
643
|
+
endColumnDrag,
|
|
644
|
+
updateColumnDropTarget,
|
|
645
|
+
commitColumnDrop,
|
|
646
|
+
onPointerMove
|
|
647
|
+
};
|
|
68
648
|
}
|