gravity-dnd 1.1.6
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/.eslintignore +3 -0
- package/.eslintrc.cjs +21 -0
- package/.storybook/main.ts +15 -0
- package/.storybook/preview.css +80 -0
- package/.storybook/preview.ts +15 -0
- package/LICENSE +373 -0
- package/README.md +292 -0
- package/index.ts +19 -0
- package/package.json +64 -0
- package/public/.gitkeep +0 -0
- package/src/Gravity.stories.ts +207 -0
- package/src/components/DragAndDrop/DragAndDrop.scss +4 -0
- package/src/components/DragAndDrop/DragAndDrop.stories.ts +787 -0
- package/src/components/DragAndDrop/DragAndDrop.visuals.css +1 -0
- package/src/components/DragAndDrop/DragAndDrop.vue +23 -0
- package/src/components/DragAndDrop.scss +4 -0
- package/src/components/DragAndDrop.visuals.css +1 -0
- package/src/components/DragAndDrop.vue +23 -0
- package/src/components/Draggable/DragDropProvider.scss +4 -0
- package/src/components/Draggable/DragDropProvider.visuals.css +1 -0
- package/src/components/Draggable/DragDropProvider.vue +11 -0
- package/src/components/Draggable/DragPreviewOverlay.scss +21 -0
- package/src/components/Draggable/DragPreviewOverlay.visuals.css +3 -0
- package/src/components/Draggable/DragPreviewOverlay.vue +41 -0
- package/src/components/Draggable/Draggable.scss +86 -0
- package/src/components/Draggable/Draggable.stories.ts +232 -0
- package/src/components/Draggable/Draggable.visuals.css +8 -0
- package/src/components/Draggable/Draggable.vue +292 -0
- package/src/components/Draggable/contracts.ts +82 -0
- package/src/components/Draggable/internalDropLayer.ts +126 -0
- package/src/components/Draggable/useDragDropContext.ts +310 -0
- package/src/components/Pool/Pool.scss +107 -0
- package/src/components/Pool/Pool.stories.ts +155 -0
- package/src/components/Pool/Pool.visuals.css +25 -0
- package/src/components/Pool/Pool.vue +198 -0
- package/src/components/Slot/Slot.scss +48 -0
- package/src/components/Slot/Slot.stories.ts +299 -0
- package/src/components/Slot/Slot.visuals.css +15 -0
- package/src/components/Slot/Slot.vue +126 -0
- package/src/styles.css +15 -0
- package/styles.css +1 -0
- package/styles.scss +6 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +21 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.st-gravity-draggable(
|
|
3
|
+
ref="rootEl"
|
|
4
|
+
:class="rootClasses"
|
|
5
|
+
:style="rootStyle"
|
|
6
|
+
@pointerdown.prevent="onPointerDown"
|
|
7
|
+
)
|
|
8
|
+
.st-gravity-draggable__content
|
|
9
|
+
slot(
|
|
10
|
+
:dragging="dragging"
|
|
11
|
+
:hovering="hovering"
|
|
12
|
+
:canDropAtHover="canDropAtHover"
|
|
13
|
+
:dropMode="dropMode"
|
|
14
|
+
)
|
|
15
|
+
DragPreviewOverlay(
|
|
16
|
+
v-if="dragDrop && dragDrop.dragState.draggableId === props.draggableId"
|
|
17
|
+
:visible="dragDrop.dragState.active && Boolean(dragDrop.dragState.item)"
|
|
18
|
+
:item="props.item"
|
|
19
|
+
:x="dragDrop.rawClientX.value"
|
|
20
|
+
:y="dragDrop.rawClientY.value"
|
|
21
|
+
:shiftX="dragDrop.dragState.shiftX"
|
|
22
|
+
:shiftY="dragDrop.dragState.shiftY"
|
|
23
|
+
:width="dragDrop.dragState.previewWidth"
|
|
24
|
+
:height="dragDrop.dragState.previewHeight"
|
|
25
|
+
)
|
|
26
|
+
slot(
|
|
27
|
+
:dragging="true"
|
|
28
|
+
:hovering="hovering"
|
|
29
|
+
:canDropAtHover="canDropAtHover"
|
|
30
|
+
:dropMode="dropMode"
|
|
31
|
+
)
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { computed, onBeforeUnmount, ref } from 'vue';
|
|
36
|
+
import { useDragDropContext } from './useDragDropContext';
|
|
37
|
+
import DragPreviewOverlay from './DragPreviewOverlay.vue';
|
|
38
|
+
import type {
|
|
39
|
+
DragDropMode,
|
|
40
|
+
DragDropSource,
|
|
41
|
+
DraggableCanDragContext,
|
|
42
|
+
DraggableDropEvent,
|
|
43
|
+
} from './contracts';
|
|
44
|
+
import './Draggable.scss';
|
|
45
|
+
|
|
46
|
+
const props = withDefaults(
|
|
47
|
+
defineProps<{
|
|
48
|
+
draggableId: string;
|
|
49
|
+
item: unknown;
|
|
50
|
+
sourceId: string;
|
|
51
|
+
sourceKind?: DragDropSource['kind'];
|
|
52
|
+
sourceIndex: number;
|
|
53
|
+
dropMode?: DragDropMode;
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
boundarySelector?: string | null;
|
|
56
|
+
canDrag?: (ctx: DraggableCanDragContext<unknown>) => boolean;
|
|
57
|
+
}>(),
|
|
58
|
+
{
|
|
59
|
+
sourceKind: 'custom',
|
|
60
|
+
dropMode: 'target',
|
|
61
|
+
disabled: false,
|
|
62
|
+
boundarySelector: null,
|
|
63
|
+
canDrag: undefined,
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const emit = defineEmits<{
|
|
68
|
+
dragStart: [payload: { draggableId: string; source: DragDropSource; item: unknown }];
|
|
69
|
+
dragMove: [payload: { draggableId: string; clientX: number; clientY: number }];
|
|
70
|
+
dragEnd: [payload: DraggableDropEvent<unknown>];
|
|
71
|
+
drop: [payload: DraggableDropEvent<unknown>];
|
|
72
|
+
cancel: [payload: { draggableId: string; source: DragDropSource; reason: 'disabled' | 'rejected' | 'out-of-bounds' }];
|
|
73
|
+
}>();
|
|
74
|
+
|
|
75
|
+
const rootEl = ref<HTMLElement | null>(null);
|
|
76
|
+
const dragDrop = useDragDropContext();
|
|
77
|
+
const isPressed = ref(false);
|
|
78
|
+
const feedback = ref<'none' | 'accepted' | 'rejected' | 'returned'>('none');
|
|
79
|
+
const floatingOffsetX = ref(0);
|
|
80
|
+
const floatingOffsetY = ref(0);
|
|
81
|
+
const dragStartClientX = ref(0);
|
|
82
|
+
const dragStartClientY = ref(0);
|
|
83
|
+
const dragStartOffsetX = ref(0);
|
|
84
|
+
const dragStartOffsetY = ref(0);
|
|
85
|
+
let feedbackTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
86
|
+
|
|
87
|
+
const source = computed<DragDropSource>(() => ({
|
|
88
|
+
containerId: props.sourceId,
|
|
89
|
+
kind: props.sourceKind,
|
|
90
|
+
index: props.sourceIndex,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const dragging = computed(() => {
|
|
94
|
+
if (!dragDrop) return false;
|
|
95
|
+
return dragDrop.isDragging(source.value);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const hovering = computed(() => {
|
|
99
|
+
if (!dragging.value) return false;
|
|
100
|
+
return !!dragDrop?.dragState.hoverTarget;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const canDropAtHover = computed(() => {
|
|
104
|
+
if (!dragging.value) return false;
|
|
105
|
+
return !!dragDrop?.dragState.hoverTarget?.accepts;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const dropMode = computed(() => props.dropMode);
|
|
109
|
+
|
|
110
|
+
const isAcceptingHover = computed(() => dragging.value && hovering.value && canDropAtHover.value);
|
|
111
|
+
const isRejectingHover = computed(() => dragging.value && hovering.value && !canDropAtHover.value);
|
|
112
|
+
const rootClasses = computed(() => ({
|
|
113
|
+
'st-gravity-draggable--pressed': isPressed.value,
|
|
114
|
+
'st-gravity-draggable--dragging': dragging.value,
|
|
115
|
+
'st-gravity-draggable--hover-accept': isAcceptingHover.value,
|
|
116
|
+
'st-gravity-draggable--hover-reject': isRejectingHover.value,
|
|
117
|
+
'st-gravity-draggable--feedback-accepted': feedback.value === 'accepted',
|
|
118
|
+
'st-gravity-draggable--feedback-rejected': feedback.value === 'rejected',
|
|
119
|
+
'st-gravity-draggable--feedback-returned': feedback.value === 'returned',
|
|
120
|
+
'st-gravity-draggable--disabled': props.disabled,
|
|
121
|
+
}));
|
|
122
|
+
const rootStyle = computed(() => {
|
|
123
|
+
const style: Record<string, string> = {};
|
|
124
|
+
if (props.dropMode === 'floating' && (floatingOffsetX.value || floatingOffsetY.value)) {
|
|
125
|
+
style.position = 'relative';
|
|
126
|
+
style.left = `${floatingOffsetX.value}px`;
|
|
127
|
+
style.top = `${floatingOffsetY.value}px`;
|
|
128
|
+
}
|
|
129
|
+
return Object.keys(style).length ? style : undefined;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function clearFeedbackTimer() {
|
|
133
|
+
if (!feedbackTimeout) return;
|
|
134
|
+
clearTimeout(feedbackTimeout);
|
|
135
|
+
feedbackTimeout = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function flashFeedback(kind: 'accepted' | 'rejected' | 'returned') {
|
|
139
|
+
clearFeedbackTimer();
|
|
140
|
+
feedback.value = kind;
|
|
141
|
+
feedbackTimeout = setTimeout(() => {
|
|
142
|
+
feedback.value = 'none';
|
|
143
|
+
feedbackTimeout = null;
|
|
144
|
+
}, 220);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getBoundaryRect(): { left: number; top: number; right: number; bottom: number } | null {
|
|
148
|
+
if (!props.boundarySelector) return null;
|
|
149
|
+
const boundaryEl = document.querySelector<HTMLElement>(props.boundarySelector);
|
|
150
|
+
if (!boundaryEl) return null;
|
|
151
|
+
const rect = boundaryEl.getBoundingClientRect();
|
|
152
|
+
return {
|
|
153
|
+
left: rect.left,
|
|
154
|
+
top: rect.top,
|
|
155
|
+
right: rect.right,
|
|
156
|
+
bottom: rect.bottom,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isOutsideBoundary(
|
|
161
|
+
boundary: { left: number; top: number; right: number; bottom: number },
|
|
162
|
+
clientX: number,
|
|
163
|
+
clientY: number
|
|
164
|
+
) {
|
|
165
|
+
return clientX < boundary.left || clientX > boundary.right || clientY < boundary.top || clientY > boundary.bottom;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onPointerMove(event: PointerEvent) {
|
|
169
|
+
if (!dragDrop?.dragState.active || dragDrop.dragState.draggableId !== props.draggableId) return;
|
|
170
|
+
emit('dragMove', {
|
|
171
|
+
draggableId: props.draggableId,
|
|
172
|
+
clientX: event.clientX,
|
|
173
|
+
clientY: event.clientY,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function teardownPointerListeners() {
|
|
178
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
179
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
180
|
+
document.removeEventListener('pointercancel', onPointerUp);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function onPointerUp(event: PointerEvent) {
|
|
184
|
+
teardownPointerListeners();
|
|
185
|
+
isPressed.value = false;
|
|
186
|
+
if (!dragDrop) return;
|
|
187
|
+
if (props.boundarySelector) {
|
|
188
|
+
const boundary = getBoundaryRect();
|
|
189
|
+
const clientX = Number.isFinite(event.clientX) && event.clientX >= 0 ? event.clientX : dragDrop.dragState.clientX;
|
|
190
|
+
const clientY = Number.isFinite(event.clientY) && event.clientY >= 0 ? event.clientY : dragDrop.dragState.clientY;
|
|
191
|
+
if (boundary && isOutsideBoundary(boundary, clientX, clientY)) {
|
|
192
|
+
dragDrop.cancelDrag();
|
|
193
|
+
flashFeedback('returned');
|
|
194
|
+
emit('cancel', {
|
|
195
|
+
draggableId: props.draggableId,
|
|
196
|
+
source: source.value,
|
|
197
|
+
reason: 'out-of-bounds',
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const endClientX = Number.isFinite(event.clientX) && event.clientX >= 0 ? event.clientX : dragDrop.dragState.clientX;
|
|
203
|
+
const endClientY = Number.isFinite(event.clientY) && event.clientY >= 0 ? event.clientY : dragDrop.dragState.clientY;
|
|
204
|
+
const result = dragDrop.endDrag<unknown>();
|
|
205
|
+
if (!result) return;
|
|
206
|
+
|
|
207
|
+
const payload: DraggableDropEvent<unknown> = {
|
|
208
|
+
...result.event,
|
|
209
|
+
accepted: result.accepted,
|
|
210
|
+
};
|
|
211
|
+
if (result.accepted && result.event.target.kind === 'floating') {
|
|
212
|
+
floatingOffsetX.value = dragStartOffsetX.value + (endClientX - dragStartClientX.value);
|
|
213
|
+
floatingOffsetY.value = dragStartOffsetY.value + (endClientY - dragStartClientY.value);
|
|
214
|
+
}
|
|
215
|
+
if (result.accepted) {
|
|
216
|
+
const droppedBackToOrigin =
|
|
217
|
+
result.event.target.containerId === payload.source.containerId && result.event.target.index === payload.source.index;
|
|
218
|
+
flashFeedback(droppedBackToOrigin ? 'returned' : 'accepted');
|
|
219
|
+
} else {
|
|
220
|
+
flashFeedback('rejected');
|
|
221
|
+
}
|
|
222
|
+
emit('dragEnd', payload);
|
|
223
|
+
if (result.accepted) {
|
|
224
|
+
emit('drop', payload);
|
|
225
|
+
} else {
|
|
226
|
+
emit('cancel', {
|
|
227
|
+
draggableId: props.draggableId,
|
|
228
|
+
source: source.value,
|
|
229
|
+
reason: 'rejected',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onPointerDown(event: PointerEvent) {
|
|
235
|
+
if (!dragDrop || props.disabled) {
|
|
236
|
+
emit('cancel', {
|
|
237
|
+
draggableId: props.draggableId,
|
|
238
|
+
source: source.value,
|
|
239
|
+
reason: 'disabled',
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (props.canDrag && !props.canDrag({ item: props.item, source: source.value })) {
|
|
244
|
+
emit('cancel', {
|
|
245
|
+
draggableId: props.draggableId,
|
|
246
|
+
source: source.value,
|
|
247
|
+
reason: 'rejected',
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const el = rootEl.value;
|
|
253
|
+
if (!el) return;
|
|
254
|
+
/* At-rest box for the cursor clone only — before press scale changes layout */
|
|
255
|
+
const rectAtRest = el.getBoundingClientRect();
|
|
256
|
+
isPressed.value = true;
|
|
257
|
+
const boundary = getBoundaryRect();
|
|
258
|
+
dragStartClientX.value = event.clientX;
|
|
259
|
+
dragStartClientY.value = event.clientY;
|
|
260
|
+
dragStartOffsetX.value = floatingOffsetX.value;
|
|
261
|
+
dragStartOffsetY.value = floatingOffsetY.value;
|
|
262
|
+
|
|
263
|
+
dragDrop.startDrag({
|
|
264
|
+
draggableId: props.draggableId,
|
|
265
|
+
mode: props.dropMode,
|
|
266
|
+
item: props.item,
|
|
267
|
+
source: source.value,
|
|
268
|
+
shiftX: event.clientX - rectAtRest.left,
|
|
269
|
+
shiftY: event.clientY - rectAtRest.top,
|
|
270
|
+
clientX: event.clientX,
|
|
271
|
+
clientY: event.clientY,
|
|
272
|
+
previewWidth: rectAtRest.width,
|
|
273
|
+
previewHeight: rectAtRest.height,
|
|
274
|
+
boundary,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
emit('dragStart', {
|
|
278
|
+
draggableId: props.draggableId,
|
|
279
|
+
source: source.value,
|
|
280
|
+
item: props.item,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
284
|
+
document.addEventListener('pointerup', onPointerUp);
|
|
285
|
+
document.addEventListener('pointercancel', onPointerUp);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
onBeforeUnmount(() => {
|
|
289
|
+
teardownPointerListeners();
|
|
290
|
+
clearFeedbackTimer();
|
|
291
|
+
});
|
|
292
|
+
</script>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type GravityContainerKind = 'slot' | 'pool' | 'custom';
|
|
2
|
+
|
|
3
|
+
export type GravityMode = 'target' | 'floating';
|
|
4
|
+
|
|
5
|
+
export interface GravitySource {
|
|
6
|
+
containerId: string;
|
|
7
|
+
kind: GravityContainerKind;
|
|
8
|
+
index: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GravityBoundary {
|
|
12
|
+
left: number;
|
|
13
|
+
top: number;
|
|
14
|
+
right: number;
|
|
15
|
+
bottom: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GravityCanDragContext<TItem> {
|
|
19
|
+
item: TItem;
|
|
20
|
+
source: GravitySource;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GravityHoverTarget {
|
|
24
|
+
id: string;
|
|
25
|
+
kind: 'slot' | 'pool';
|
|
26
|
+
containerId: string;
|
|
27
|
+
index: number;
|
|
28
|
+
accepts: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GravityDropTarget {
|
|
32
|
+
kind: 'slot' | 'pool' | 'floating';
|
|
33
|
+
containerId: string | null;
|
|
34
|
+
index: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GravityDropEvent<TItem> {
|
|
38
|
+
draggableId: string;
|
|
39
|
+
item: TItem;
|
|
40
|
+
source: GravitySource;
|
|
41
|
+
target: GravityDropTarget;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GravityDraggableDropEvent<TItem> extends GravityDropEvent<TItem> {
|
|
45
|
+
accepted: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type GravitySlotCollisionRule = 'replace' | 'swap' | 'reject';
|
|
49
|
+
|
|
50
|
+
export interface GravitySlotDropEvent<TItem> extends GravityDropEvent<TItem> {
|
|
51
|
+
slotId: string;
|
|
52
|
+
swap: boolean;
|
|
53
|
+
collision: GravitySlotCollisionRule;
|
|
54
|
+
replacedItem?: TItem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface GravityPoolReorderEvent<TItem> extends GravityDropEvent<TItem> {
|
|
58
|
+
poolId: string;
|
|
59
|
+
fromIndex: number;
|
|
60
|
+
toIndex: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GravityPoolReceiveEvent<TItem> extends GravityDropEvent<TItem> {
|
|
64
|
+
poolId: string;
|
|
65
|
+
insertIndex: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* Backwards-compatible aliases while the old DragDrop naming is phased out.
|
|
70
|
+
*/
|
|
71
|
+
export type DragDropContainerKind = GravityContainerKind;
|
|
72
|
+
export type DragDropMode = GravityMode;
|
|
73
|
+
export type DragDropSource = GravitySource;
|
|
74
|
+
export type DragDropBoundary = GravityBoundary;
|
|
75
|
+
export type DraggableCanDragContext<TItem> = GravityCanDragContext<TItem>;
|
|
76
|
+
export type DragDropHoverTarget = GravityHoverTarget;
|
|
77
|
+
export type DragDropDropTarget = GravityDropTarget;
|
|
78
|
+
export type DragDropDropEvent<TItem> = GravityDropEvent<TItem>;
|
|
79
|
+
export type DraggableDropEvent<TItem> = GravityDraggableDropEvent<TItem>;
|
|
80
|
+
export type SlotDropEvent<TItem> = GravitySlotDropEvent<TItem>;
|
|
81
|
+
export type PoolReorderEvent<TItem> = GravityPoolReorderEvent<TItem>;
|
|
82
|
+
export type PoolReceiveEvent<TItem> = GravityPoolReceiveEvent<TItem>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DragDropDropEvent,
|
|
3
|
+
DragDropDropTarget,
|
|
4
|
+
DragDropHoverTarget,
|
|
5
|
+
DragDropMode,
|
|
6
|
+
DragDropSource,
|
|
7
|
+
} from './contracts';
|
|
8
|
+
|
|
9
|
+
export interface InternalDropTargetRegistration {
|
|
10
|
+
id: string;
|
|
11
|
+
kind: 'slot' | 'pool';
|
|
12
|
+
containerId: string;
|
|
13
|
+
resolveHover: (
|
|
14
|
+
clientX: number,
|
|
15
|
+
clientY: number,
|
|
16
|
+
context: {
|
|
17
|
+
item: unknown;
|
|
18
|
+
source: DragDropSource;
|
|
19
|
+
}
|
|
20
|
+
) => Omit<DragDropHoverTarget, 'id' | 'kind' | 'containerId'> | null;
|
|
21
|
+
onDrop?: (payload: DragDropDropEvent<unknown>) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ResolveDropTargetInput {
|
|
25
|
+
mode: DragDropMode;
|
|
26
|
+
hoverTarget: DragDropHoverTarget | null;
|
|
27
|
+
source: DragDropSource | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FLOATING_TARGET: DragDropDropTarget = {
|
|
31
|
+
kind: 'floating',
|
|
32
|
+
containerId: null,
|
|
33
|
+
index: -1,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function createInternalDropLayer() {
|
|
37
|
+
const targets = new Map<string, InternalDropTargetRegistration>();
|
|
38
|
+
|
|
39
|
+
function registerTarget(target: InternalDropTargetRegistration) {
|
|
40
|
+
targets.set(target.id, target);
|
|
41
|
+
return () => {
|
|
42
|
+
targets.delete(target.id);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function detectHoverTarget(
|
|
47
|
+
clientX: number,
|
|
48
|
+
clientY: number,
|
|
49
|
+
context: {
|
|
50
|
+
item: unknown;
|
|
51
|
+
source: DragDropSource;
|
|
52
|
+
}
|
|
53
|
+
): DragDropHoverTarget | null {
|
|
54
|
+
for (const target of targets.values()) {
|
|
55
|
+
const resolved = target.resolveHover(clientX, clientY, context);
|
|
56
|
+
if (!resolved) continue;
|
|
57
|
+
return {
|
|
58
|
+
id: target.id,
|
|
59
|
+
kind: target.kind,
|
|
60
|
+
containerId: target.containerId,
|
|
61
|
+
index: resolved.index,
|
|
62
|
+
accepts: resolved.accepts,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveDropTarget(input: ResolveDropTargetInput): { target: DragDropDropTarget; accepted: boolean } {
|
|
69
|
+
if (
|
|
70
|
+
input.hoverTarget &&
|
|
71
|
+
input.source &&
|
|
72
|
+
input.hoverTarget.containerId === input.source.containerId &&
|
|
73
|
+
input.hoverTarget.kind === input.source.kind &&
|
|
74
|
+
input.hoverTarget.index === input.source.index
|
|
75
|
+
) {
|
|
76
|
+
return {
|
|
77
|
+
target: {
|
|
78
|
+
kind: input.hoverTarget.kind,
|
|
79
|
+
containerId: input.hoverTarget.containerId,
|
|
80
|
+
index: input.hoverTarget.index,
|
|
81
|
+
},
|
|
82
|
+
accepted: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (input.hoverTarget && input.hoverTarget.accepts) {
|
|
87
|
+
return {
|
|
88
|
+
target: {
|
|
89
|
+
kind: input.hoverTarget.kind,
|
|
90
|
+
containerId: input.hoverTarget.containerId,
|
|
91
|
+
index: input.hoverTarget.index,
|
|
92
|
+
},
|
|
93
|
+
accepted: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (input.mode === 'floating') {
|
|
98
|
+
return {
|
|
99
|
+
target: FLOATING_TARGET,
|
|
100
|
+
accepted: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
target: FLOATING_TARGET,
|
|
106
|
+
accepted: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getTarget(targetId: string | null | undefined) {
|
|
111
|
+
if (!targetId) return null;
|
|
112
|
+
return targets.get(targetId) ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function clear() {
|
|
116
|
+
targets.clear();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
registerTarget,
|
|
121
|
+
detectHoverTarget,
|
|
122
|
+
resolveDropTarget,
|
|
123
|
+
getTarget,
|
|
124
|
+
clear,
|
|
125
|
+
};
|
|
126
|
+
}
|