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,310 @@
|
|
|
1
|
+
import { inject, onBeforeUnmount, provide, reactive, ref, shallowRef, type InjectionKey, type Ref } from 'vue';
|
|
2
|
+
import type {
|
|
3
|
+
DragDropBoundary,
|
|
4
|
+
DragDropDropEvent,
|
|
5
|
+
DragDropHoverTarget,
|
|
6
|
+
DragDropMode,
|
|
7
|
+
DragDropSource,
|
|
8
|
+
} from './contracts';
|
|
9
|
+
import { createInternalDropLayer, type InternalDropTargetRegistration } from './internalDropLayer';
|
|
10
|
+
|
|
11
|
+
export interface ActiveDrag<TItem = unknown> {
|
|
12
|
+
active: boolean;
|
|
13
|
+
draggableId: string | null;
|
|
14
|
+
mode: DragDropMode;
|
|
15
|
+
item: TItem | null;
|
|
16
|
+
source: DragDropSource | null;
|
|
17
|
+
clientX: number;
|
|
18
|
+
clientY: number;
|
|
19
|
+
shiftX: number;
|
|
20
|
+
shiftY: number;
|
|
21
|
+
previewWidth: number;
|
|
22
|
+
previewHeight: number;
|
|
23
|
+
hoverTarget: DragDropHoverTarget | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DragStartPayload<TItem> {
|
|
27
|
+
draggableId: string;
|
|
28
|
+
mode: DragDropMode;
|
|
29
|
+
item: TItem;
|
|
30
|
+
source: DragDropSource;
|
|
31
|
+
shiftX: number;
|
|
32
|
+
shiftY: number;
|
|
33
|
+
clientX: number;
|
|
34
|
+
clientY: number;
|
|
35
|
+
previewWidth: number;
|
|
36
|
+
previewHeight: number;
|
|
37
|
+
boundary: DragDropBoundary | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface DragEndResult<TItem> {
|
|
41
|
+
event: DragDropDropEvent<TItem>;
|
|
42
|
+
accepted: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface InternalDragDropContext {
|
|
46
|
+
dragState: ActiveDrag;
|
|
47
|
+
rawClientX: Ref<number>;
|
|
48
|
+
rawClientY: Ref<number>;
|
|
49
|
+
registerTarget: (target: InternalDropTargetRegistration) => () => void;
|
|
50
|
+
startDrag: <TItem>(payload: DragStartPayload<TItem>) => void;
|
|
51
|
+
endDrag: <TItem>() => DragEndResult<TItem> | null;
|
|
52
|
+
cancelDrag: () => void;
|
|
53
|
+
isDragging: (source: DragDropSource) => boolean;
|
|
54
|
+
isHovering: (containerId: string, index?: number) => boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DRAG_DROP_CONTEXT_KEY: InjectionKey<InternalDragDropContext> = Symbol('gravity.dragDropContext');
|
|
58
|
+
|
|
59
|
+
export function provideDragDropContext() {
|
|
60
|
+
const dragState = reactive<ActiveDrag>({
|
|
61
|
+
active: false,
|
|
62
|
+
draggableId: null,
|
|
63
|
+
mode: 'target',
|
|
64
|
+
item: null,
|
|
65
|
+
source: null,
|
|
66
|
+
clientX: 0,
|
|
67
|
+
clientY: 0,
|
|
68
|
+
shiftX: 0,
|
|
69
|
+
shiftY: 0,
|
|
70
|
+
previewWidth: 0,
|
|
71
|
+
previewHeight: 0,
|
|
72
|
+
hoverTarget: null,
|
|
73
|
+
});
|
|
74
|
+
const activeBoundary = ref<DragDropBoundary | null>(null);
|
|
75
|
+
const dropLayer = createInternalDropLayer();
|
|
76
|
+
|
|
77
|
+
// Raw (non-reactive) coordinates updated every pointermove for preview position.
|
|
78
|
+
// Only the preview overlay reads these via shallowRef to avoid triggering the
|
|
79
|
+
// full reactive cascade on every pixel move.
|
|
80
|
+
const rawClientX = shallowRef(0);
|
|
81
|
+
const rawClientY = shallowRef(0);
|
|
82
|
+
let _rafId: number | null = null;
|
|
83
|
+
let _pendingX = 0;
|
|
84
|
+
let _pendingY = 0;
|
|
85
|
+
let _hoverDirty = false;
|
|
86
|
+
|
|
87
|
+
function reset() {
|
|
88
|
+
dragState.active = false;
|
|
89
|
+
dragState.draggableId = null;
|
|
90
|
+
dragState.item = null;
|
|
91
|
+
dragState.mode = 'target';
|
|
92
|
+
dragState.source = null;
|
|
93
|
+
dragState.clientX = 0;
|
|
94
|
+
dragState.clientY = 0;
|
|
95
|
+
dragState.shiftX = 0;
|
|
96
|
+
dragState.shiftY = 0;
|
|
97
|
+
dragState.previewWidth = 0;
|
|
98
|
+
dragState.previewHeight = 0;
|
|
99
|
+
dragState.hoverTarget = null;
|
|
100
|
+
activeBoundary.value = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function registerTarget(target: InternalDropTargetRegistration) {
|
|
104
|
+
return dropLayer.registerTarget(target);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function withinBoundary(clientX: number, clientY: number): boolean {
|
|
108
|
+
if (!activeBoundary.value) return true;
|
|
109
|
+
return (
|
|
110
|
+
clientX >= activeBoundary.value.left &&
|
|
111
|
+
clientX <= activeBoundary.value.right &&
|
|
112
|
+
clientY >= activeBoundary.value.top &&
|
|
113
|
+
clientY <= activeBoundary.value.bottom
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function processFrame() {
|
|
118
|
+
_rafId = null;
|
|
119
|
+
if (!dragState.active) return;
|
|
120
|
+
|
|
121
|
+
const cx = _pendingX;
|
|
122
|
+
const cy = _pendingY;
|
|
123
|
+
|
|
124
|
+
// Always update preview position (cheap shallowRef trigger)
|
|
125
|
+
rawClientX.value = cx;
|
|
126
|
+
rawClientY.value = cy;
|
|
127
|
+
|
|
128
|
+
// Only run expensive hover detection when coordinates actually changed
|
|
129
|
+
if (!_hoverDirty) return;
|
|
130
|
+
_hoverDirty = false;
|
|
131
|
+
|
|
132
|
+
// Update reactive coords (used by endDrag for final position)
|
|
133
|
+
dragState.clientX = cx;
|
|
134
|
+
dragState.clientY = cy;
|
|
135
|
+
|
|
136
|
+
if (!withinBoundary(cx, cy)) {
|
|
137
|
+
if (dragState.hoverTarget !== null) dragState.hoverTarget = null;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!dragState.item || !dragState.source) {
|
|
141
|
+
if (dragState.hoverTarget !== null) dragState.hoverTarget = null;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const newTarget = dropLayer.detectHoverTarget(cx, cy, {
|
|
146
|
+
item: dragState.item,
|
|
147
|
+
source: dragState.source,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Only trigger reactive update if hover target actually changed
|
|
151
|
+
const prev = dragState.hoverTarget;
|
|
152
|
+
if (
|
|
153
|
+
newTarget?.id !== prev?.id ||
|
|
154
|
+
newTarget?.index !== prev?.index ||
|
|
155
|
+
newTarget?.accepts !== prev?.accepts
|
|
156
|
+
) {
|
|
157
|
+
dragState.hoverTarget = newTarget;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function onPointerMove(event: PointerEvent) {
|
|
162
|
+
if (!dragState.active) return;
|
|
163
|
+
|
|
164
|
+
const cx = event.clientX;
|
|
165
|
+
const cy = event.clientY;
|
|
166
|
+
|
|
167
|
+
// Mark dirty if coordinates changed enough to warrant hover re-evaluation
|
|
168
|
+
if (cx !== _pendingX || cy !== _pendingY) {
|
|
169
|
+
_hoverDirty = true;
|
|
170
|
+
}
|
|
171
|
+
_pendingX = cx;
|
|
172
|
+
_pendingY = cy;
|
|
173
|
+
|
|
174
|
+
// Coalesce into a single RAF — preview moves at display refresh rate,
|
|
175
|
+
// hover detection runs at most once per frame instead of per pointermove.
|
|
176
|
+
if (_rafId === null) {
|
|
177
|
+
_rafId = requestAnimationFrame(processFrame);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function teardownListeners() {
|
|
182
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
183
|
+
if (_rafId !== null) {
|
|
184
|
+
cancelAnimationFrame(_rafId);
|
|
185
|
+
_rafId = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function startDrag<TItem>(payload: DragStartPayload<TItem>) {
|
|
190
|
+
dragState.active = true;
|
|
191
|
+
dragState.draggableId = payload.draggableId;
|
|
192
|
+
dragState.mode = payload.mode;
|
|
193
|
+
dragState.item = payload.item;
|
|
194
|
+
dragState.source = payload.source;
|
|
195
|
+
dragState.clientX = payload.clientX;
|
|
196
|
+
dragState.clientY = payload.clientY;
|
|
197
|
+
dragState.shiftX = payload.shiftX;
|
|
198
|
+
dragState.shiftY = payload.shiftY;
|
|
199
|
+
dragState.previewWidth = payload.previewWidth;
|
|
200
|
+
dragState.previewHeight = payload.previewHeight;
|
|
201
|
+
dragState.hoverTarget = null;
|
|
202
|
+
activeBoundary.value = payload.boundary;
|
|
203
|
+
_pendingX = payload.clientX;
|
|
204
|
+
_pendingY = payload.clientY;
|
|
205
|
+
rawClientX.value = payload.clientX;
|
|
206
|
+
rawClientY.value = payload.clientY;
|
|
207
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function endDrag<TItem>() {
|
|
211
|
+
if (!dragState.active || !dragState.item || !dragState.source || !dragState.draggableId) {
|
|
212
|
+
teardownListeners();
|
|
213
|
+
reset();
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const hoveredTarget = dropLayer.getTarget(dragState.hoverTarget?.id);
|
|
218
|
+
const { target, accepted } = dropLayer.resolveDropTarget({
|
|
219
|
+
mode: dragState.mode,
|
|
220
|
+
hoverTarget: dragState.hoverTarget,
|
|
221
|
+
source: dragState.source,
|
|
222
|
+
});
|
|
223
|
+
const payload: DragDropDropEvent<TItem> = {
|
|
224
|
+
draggableId: dragState.draggableId,
|
|
225
|
+
item: dragState.item as TItem,
|
|
226
|
+
source: dragState.source,
|
|
227
|
+
target,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (accepted && hoveredTarget?.onDrop) {
|
|
231
|
+
hoveredTarget.onDrop(payload as DragDropDropEvent<unknown>);
|
|
232
|
+
}
|
|
233
|
+
teardownListeners();
|
|
234
|
+
reset();
|
|
235
|
+
return {
|
|
236
|
+
event: payload,
|
|
237
|
+
accepted,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function cancelDrag() {
|
|
242
|
+
teardownListeners();
|
|
243
|
+
reset();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isDragging(source: DragDropSource): boolean {
|
|
247
|
+
return (
|
|
248
|
+
dragState.active &&
|
|
249
|
+
dragState.source?.containerId === source.containerId &&
|
|
250
|
+
dragState.source?.index === source.index &&
|
|
251
|
+
dragState.source?.kind === source.kind
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isHovering(containerId: string, index?: number): boolean {
|
|
256
|
+
if (!dragState.active || !dragState.hoverTarget) return false;
|
|
257
|
+
if (dragState.hoverTarget.containerId !== containerId) return false;
|
|
258
|
+
if (typeof index !== 'number') return true;
|
|
259
|
+
return dragState.hoverTarget.index === index;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
provide(DRAG_DROP_CONTEXT_KEY, {
|
|
263
|
+
dragState,
|
|
264
|
+
rawClientX,
|
|
265
|
+
rawClientY,
|
|
266
|
+
registerTarget,
|
|
267
|
+
startDrag,
|
|
268
|
+
endDrag,
|
|
269
|
+
cancelDrag,
|
|
270
|
+
isDragging,
|
|
271
|
+
isHovering,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
onBeforeUnmount(() => {
|
|
275
|
+
teardownListeners();
|
|
276
|
+
dropLayer.clear();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
dragState,
|
|
281
|
+
cancelDrag,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function useDragDropContext() {
|
|
286
|
+
const ctx = inject(DRAG_DROP_CONTEXT_KEY, null);
|
|
287
|
+
return ctx;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function useDropTargetRegistration(target: InternalDropTargetRegistration, enabled: Ref<boolean>) {
|
|
291
|
+
const ctx = useDragDropContext();
|
|
292
|
+
if (!ctx) return;
|
|
293
|
+
|
|
294
|
+
let unregister = () => {};
|
|
295
|
+
if (enabled.value) {
|
|
296
|
+
unregister = ctx.registerTarget(target);
|
|
297
|
+
}
|
|
298
|
+
onBeforeUnmount(() => {
|
|
299
|
+
unregister();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
refresh() {
|
|
304
|
+
unregister();
|
|
305
|
+
if (!enabled.value) return;
|
|
306
|
+
unregister = ctx.registerTarget(target);
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
.st-gravity-pool {
|
|
2
|
+
border-radius: 8px;
|
|
3
|
+
min-height: 76px;
|
|
4
|
+
padding: 8px;
|
|
5
|
+
transform-origin: center;
|
|
6
|
+
transition:
|
|
7
|
+
background-color 120ms ease,
|
|
8
|
+
border-color 120ms ease,
|
|
9
|
+
box-shadow 120ms ease,
|
|
10
|
+
transform 120ms ease;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.st-gravity-pool--hovering {
|
|
14
|
+
transform: translateY(-1px);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.st-gravity-pool--accepting {
|
|
18
|
+
animation: gravity-pool-accepting 170ms ease-out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.st-gravity-pool--rejecting {
|
|
22
|
+
animation: gravity-pool-rejecting 190ms ease-out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.st-gravity-pool__list {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
gap: 6px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.st-gravity-pool__entry {
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.st-gravity-pool__drop-indicator {
|
|
36
|
+
position: relative;
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
width: 56px;
|
|
39
|
+
min-height: 30px;
|
|
40
|
+
border-radius: 6px;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
animation: gravity-pool-insert-indicator 130ms ease-out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.st-gravity-pool__drop-indicator::before {
|
|
46
|
+
content: '';
|
|
47
|
+
position: absolute;
|
|
48
|
+
inset: 0;
|
|
49
|
+
border-radius: 999px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.gravity-pool-reorder-move {
|
|
53
|
+
transition: transform 180ms ease;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.gravity-pool-reorder-enter-active {
|
|
57
|
+
transition: opacity 170ms ease, transform 170ms ease;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.gravity-pool-reorder-leave-active {
|
|
61
|
+
position: absolute;
|
|
62
|
+
opacity: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.gravity-pool-reorder-enter-from,
|
|
66
|
+
.gravity-pool-reorder-leave-to {
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transform: scale(0.92);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@keyframes gravity-pool-accepting {
|
|
72
|
+
0% {
|
|
73
|
+
transform: translateY(0);
|
|
74
|
+
}
|
|
75
|
+
60% {
|
|
76
|
+
transform: translateY(-1px) scale(1.005);
|
|
77
|
+
}
|
|
78
|
+
100% {
|
|
79
|
+
transform: translateY(-1px);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes gravity-pool-rejecting {
|
|
84
|
+
0% {
|
|
85
|
+
transform: translateX(0);
|
|
86
|
+
}
|
|
87
|
+
30% {
|
|
88
|
+
transform: translateX(-2px);
|
|
89
|
+
}
|
|
90
|
+
60% {
|
|
91
|
+
transform: translateX(2px);
|
|
92
|
+
}
|
|
93
|
+
100% {
|
|
94
|
+
transform: translateX(0);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes gravity-pool-insert-indicator {
|
|
99
|
+
0% {
|
|
100
|
+
opacity: 0;
|
|
101
|
+
transform: scaleY(0.65);
|
|
102
|
+
}
|
|
103
|
+
100% {
|
|
104
|
+
opacity: 1;
|
|
105
|
+
transform: scaleY(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import GravityProvider from '../Draggable/DragDropProvider.vue';
|
|
4
|
+
import Draggable from '../Draggable/Draggable.vue';
|
|
5
|
+
import Pool from './Pool.vue';
|
|
6
|
+
import type { GravityPoolReceiveEvent, GravityPoolReorderEvent } from '../Draggable/contracts';
|
|
7
|
+
|
|
8
|
+
interface DemoItem {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof Pool> = {
|
|
14
|
+
title: 'Gravity/Pool',
|
|
15
|
+
component: Pool,
|
|
16
|
+
tags: ['autodocs'],
|
|
17
|
+
parameters: { layout: 'centered' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof Pool>;
|
|
22
|
+
|
|
23
|
+
export const ReorderAndReceive: Story = {
|
|
24
|
+
render: () => ({
|
|
25
|
+
setup() {
|
|
26
|
+
const pool = ref<DemoItem[]>([
|
|
27
|
+
{ id: 'pool-a', label: 'A' },
|
|
28
|
+
{ id: 'pool-b', label: 'B' },
|
|
29
|
+
{ id: 'pool-c', label: 'C' },
|
|
30
|
+
{ id: 'pool-d', label: 'D' },
|
|
31
|
+
{ id: 'pool-e', label: 'E' },
|
|
32
|
+
{ id: 'pool-f', label: 'F' },
|
|
33
|
+
{ id: 'pool-g', label: 'G' },
|
|
34
|
+
{ id: 'pool-h', label: 'H' },
|
|
35
|
+
{ id: 'pool-i', label: 'I' },
|
|
36
|
+
{ id: 'pool-j', label: 'J' },
|
|
37
|
+
{ id: 'pool-k', label: 'K' },
|
|
38
|
+
{ id: 'pool-l', label: 'L' },
|
|
39
|
+
]);
|
|
40
|
+
const stash = ref<DemoItem[]>([
|
|
41
|
+
{ id: 'stash-m', label: 'M' },
|
|
42
|
+
{ id: 'stash-n', label: 'N' },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function takeFromSource(sourceContainerId: string, sourceIndex: number): DemoItem | null {
|
|
46
|
+
if (sourceContainerId === 'pool-main') {
|
|
47
|
+
const [moved] = pool.value.splice(sourceIndex, 1);
|
|
48
|
+
return moved || null;
|
|
49
|
+
}
|
|
50
|
+
if (sourceContainerId === 'stash-main') {
|
|
51
|
+
const [moved] = stash.value.splice(sourceIndex, 1);
|
|
52
|
+
return moved || null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function onPoolReorder(event: GravityPoolReorderEvent<unknown>) {
|
|
58
|
+
if (event.fromIndex === event.toIndex) return;
|
|
59
|
+
const [moved] = pool.value.splice(event.fromIndex, 1);
|
|
60
|
+
if (!moved) return;
|
|
61
|
+
let insertIndex = event.toIndex;
|
|
62
|
+
if (event.fromIndex < event.toIndex) insertIndex -= 1;
|
|
63
|
+
pool.value.splice(Math.max(0, Math.min(insertIndex, pool.value.length)), 0, moved as DemoItem);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function onPoolReceive(event: GravityPoolReceiveEvent<unknown>) {
|
|
67
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
68
|
+
if (!moved) return;
|
|
69
|
+
const insertIndex = Math.max(0, Math.min(event.insertIndex, pool.value.length));
|
|
70
|
+
pool.value.splice(insertIndex, 0, moved);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
pool,
|
|
75
|
+
stash,
|
|
76
|
+
onPoolReorder,
|
|
77
|
+
onPoolReceive,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
components: { GravityProvider, Draggable, Pool },
|
|
81
|
+
template: `
|
|
82
|
+
<div style="width: 560px; max-width: 96vw; font-family: system-ui, sans-serif;">
|
|
83
|
+
<GravityProvider>
|
|
84
|
+
<div style="display: grid; gap: 16px;">
|
|
85
|
+
<div>
|
|
86
|
+
<div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">External source</div>
|
|
87
|
+
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
|
88
|
+
<Draggable
|
|
89
|
+
v-for="(item, index) in stash"
|
|
90
|
+
:key="item.id"
|
|
91
|
+
:draggable-id="'stash-item-' + item.id"
|
|
92
|
+
:item="item"
|
|
93
|
+
source-id="stash-main"
|
|
94
|
+
source-kind="custom"
|
|
95
|
+
:source-index="index"
|
|
96
|
+
>
|
|
97
|
+
<template #default="{ dragging }">
|
|
98
|
+
<div
|
|
99
|
+
:style="{
|
|
100
|
+
padding: '6px 10px',
|
|
101
|
+
borderRadius: '6px',
|
|
102
|
+
border: '1px solid #d1d5db',
|
|
103
|
+
background: '#f9fafb',
|
|
104
|
+
opacity: dragging ? .9 : 1
|
|
105
|
+
}"
|
|
106
|
+
>
|
|
107
|
+
{{ item.label }}
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
</Draggable>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div>
|
|
115
|
+
<div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Pool (reorderable + receives)</div>
|
|
116
|
+
<Pool
|
|
117
|
+
pool-id="pool-main"
|
|
118
|
+
:items="pool"
|
|
119
|
+
@reorder="onPoolReorder"
|
|
120
|
+
@receive="onPoolReceive"
|
|
121
|
+
>
|
|
122
|
+
<template #item="{ item, index }">
|
|
123
|
+
<Draggable
|
|
124
|
+
:draggable-id="'pool-item-' + item.id"
|
|
125
|
+
:item="item"
|
|
126
|
+
source-id="pool-main"
|
|
127
|
+
source-kind="pool"
|
|
128
|
+
:source-index="index"
|
|
129
|
+
>
|
|
130
|
+
<template #default="{ dragging }">
|
|
131
|
+
<div
|
|
132
|
+
:style="{
|
|
133
|
+
padding: '6px 10px',
|
|
134
|
+
minWidth: '56px',
|
|
135
|
+
textAlign: 'center',
|
|
136
|
+
borderRadius: '6px',
|
|
137
|
+
border: '1px solid #d1d5db',
|
|
138
|
+
background: '#f9fafb',
|
|
139
|
+
opacity: dragging ? .9 : 1
|
|
140
|
+
}"
|
|
141
|
+
>
|
|
142
|
+
{{ item.label }}
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
145
|
+
</Draggable>
|
|
146
|
+
</template>
|
|
147
|
+
</Pool>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
</GravityProvider>
|
|
152
|
+
</div>
|
|
153
|
+
`,
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.st-gravity-pool {
|
|
2
|
+
border: 1px solid #d1d5db;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.st-gravity-pool--accepting {
|
|
6
|
+
background: #f0fdf4;
|
|
7
|
+
border-color: #86efac;
|
|
8
|
+
box-shadow: 0 0 0 2px rgb(134 239 172 / 22%);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.st-gravity-pool--rejecting {
|
|
12
|
+
background: #fef2f2;
|
|
13
|
+
border-color: #f87171;
|
|
14
|
+
box-shadow: 0 0 0 2px rgb(248 113 113 / 22%);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.st-gravity-pool__drop-indicator {
|
|
18
|
+
border: 1px solid #22c55e;
|
|
19
|
+
background: rgb(255 255 255 / 35%);
|
|
20
|
+
box-shadow: 0 0 0 2px rgb(34 197 94 / 35%);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.st-gravity-pool__drop-indicator::before {
|
|
24
|
+
background: transparent;
|
|
25
|
+
}
|