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,787 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import DragAndDrop from './DragAndDrop.vue';
|
|
4
|
+
import Draggable from '../Draggable/Draggable.vue';
|
|
5
|
+
import Slot from '../Slot/Slot.vue';
|
|
6
|
+
import Pool from '../Pool/Pool.vue';
|
|
7
|
+
import type { PoolReceiveEvent, PoolReorderEvent, SlotDropEvent } from '../Draggable/contracts';
|
|
8
|
+
|
|
9
|
+
interface DemoItem {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SLOT_COUNT = 4;
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof DragAndDrop> = {
|
|
17
|
+
title: 'Gravity/DragAndDrop',
|
|
18
|
+
component: DragAndDrop,
|
|
19
|
+
tags: ['autodocs'],
|
|
20
|
+
parameters: { layout: 'centered' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof DragAndDrop>;
|
|
25
|
+
|
|
26
|
+
export const Combined: Story = {
|
|
27
|
+
render: () => ({
|
|
28
|
+
setup() {
|
|
29
|
+
const slots = ref<(DemoItem | null)[]>([{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }, null, null]);
|
|
30
|
+
const pool = ref<DemoItem[]>([
|
|
31
|
+
{ id: 'c', label: 'C' },
|
|
32
|
+
{ id: 'd', label: 'D' },
|
|
33
|
+
{ id: 'e', label: 'E' },
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function takeFromSource(sourceContainerId: string, sourceIndex: number): DemoItem | null {
|
|
37
|
+
if (sourceContainerId.startsWith('slot-')) {
|
|
38
|
+
const slotIndex = Number(sourceContainerId.replace('slot-', ''));
|
|
39
|
+
const moved = slots.value[slotIndex] || null;
|
|
40
|
+
slots.value[slotIndex] = null;
|
|
41
|
+
return moved;
|
|
42
|
+
}
|
|
43
|
+
if (sourceContainerId === 'pool-main') {
|
|
44
|
+
const [moved] = pool.value.splice(sourceIndex, 1);
|
|
45
|
+
return moved || null;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onSlotDrop(event: SlotDropEvent<unknown>) {
|
|
51
|
+
const slotIndex = Number(event.slotId.replace('slot-', ''));
|
|
52
|
+
if (!Number.isFinite(slotIndex) || slotIndex < 0 || slotIndex >= SLOT_COUNT) return;
|
|
53
|
+
if (event.source.containerId === event.slotId) return;
|
|
54
|
+
if (event.swap && event.source.kind === 'slot') {
|
|
55
|
+
const sourceSlotIndex = Number(event.source.containerId.replace('slot-', ''));
|
|
56
|
+
if (!Number.isFinite(sourceSlotIndex) || sourceSlotIndex < 0 || sourceSlotIndex >= SLOT_COUNT) return;
|
|
57
|
+
const moved = slots.value[sourceSlotIndex];
|
|
58
|
+
if (!moved) return;
|
|
59
|
+
const displaced = slots.value[slotIndex];
|
|
60
|
+
slots.value[slotIndex] = moved;
|
|
61
|
+
slots.value[sourceSlotIndex] = displaced ?? null;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
66
|
+
if (!moved) return;
|
|
67
|
+
const displaced = slots.value[slotIndex];
|
|
68
|
+
slots.value[slotIndex] = moved;
|
|
69
|
+
if (displaced) pool.value.push(displaced);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onPoolReorder(event: PoolReorderEvent<unknown>) {
|
|
73
|
+
if (event.fromIndex === event.toIndex) return;
|
|
74
|
+
const [moved] = pool.value.splice(event.fromIndex, 1);
|
|
75
|
+
if (!moved) return;
|
|
76
|
+
let insertIndex = event.toIndex;
|
|
77
|
+
if (event.fromIndex < event.toIndex) insertIndex -= 1;
|
|
78
|
+
pool.value.splice(Math.max(0, Math.min(insertIndex, pool.value.length)), 0, moved as DemoItem);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onPoolReceive(event: PoolReceiveEvent<unknown>) {
|
|
82
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
83
|
+
if (!moved) return;
|
|
84
|
+
const insertIndex = Math.max(0, Math.min(event.insertIndex, pool.value.length));
|
|
85
|
+
pool.value.splice(insertIndex, 0, moved);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
slots,
|
|
90
|
+
pool,
|
|
91
|
+
SLOT_COUNT,
|
|
92
|
+
onSlotDrop,
|
|
93
|
+
onPoolReorder,
|
|
94
|
+
onPoolReceive,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
components: { DragAndDrop, Draggable, Slot, Pool },
|
|
98
|
+
template: `
|
|
99
|
+
<div style="width: 560px; max-width: 96vw; font-family: system-ui, sans-serif;">
|
|
100
|
+
<DragAndDrop>
|
|
101
|
+
<div style="display: grid; gap: 16px;">
|
|
102
|
+
<div>
|
|
103
|
+
<div style="margin-bottom: 8px; font-size: 14px; font-weight: 600;">Slots</div>
|
|
104
|
+
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;">
|
|
105
|
+
<Slot
|
|
106
|
+
v-for="idx in SLOT_COUNT"
|
|
107
|
+
:key="'slot-' + idx"
|
|
108
|
+
:slot-id="'slot-' + (idx - 1)"
|
|
109
|
+
@drop="onSlotDrop"
|
|
110
|
+
>
|
|
111
|
+
<template #default="{ hovering }">
|
|
112
|
+
<div
|
|
113
|
+
style="min-height: 56px; display: flex; align-items: center; justify-content: center; border-radius: 8px;"
|
|
114
|
+
:style="{ background: hovering ? '#ecfeff' : '#ffffff' }"
|
|
115
|
+
>
|
|
116
|
+
<Draggable
|
|
117
|
+
v-if="slots[idx - 1]"
|
|
118
|
+
:draggable-id="'slot-item-' + (idx - 1)"
|
|
119
|
+
:item="slots[idx - 1]"
|
|
120
|
+
:source-id="'slot-' + (idx - 1)"
|
|
121
|
+
source-kind="slot"
|
|
122
|
+
:source-index="0"
|
|
123
|
+
>
|
|
124
|
+
<template #default="{ dragging }">
|
|
125
|
+
<div
|
|
126
|
+
:style="{
|
|
127
|
+
padding: '6px 10px',
|
|
128
|
+
borderRadius: '6px',
|
|
129
|
+
border: '1px solid #d1d5db',
|
|
130
|
+
background: '#f9fafb',
|
|
131
|
+
opacity: dragging ? .9 : 1
|
|
132
|
+
}"
|
|
133
|
+
>
|
|
134
|
+
{{ slots[idx - 1]?.label }}
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
137
|
+
</Draggable>
|
|
138
|
+
<span v-else style="font-size: 12px; color: #64748b;">Drop item</span>
|
|
139
|
+
</div>
|
|
140
|
+
</template>
|
|
141
|
+
</Slot>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div>
|
|
146
|
+
<div style="margin-bottom: 8px; font-size: 14px; font-weight: 600;">Pool</div>
|
|
147
|
+
<Pool
|
|
148
|
+
pool-id="pool-main"
|
|
149
|
+
:items="pool"
|
|
150
|
+
@reorder="onPoolReorder"
|
|
151
|
+
@receive="onPoolReceive"
|
|
152
|
+
>
|
|
153
|
+
<template #item="{ item, index }">
|
|
154
|
+
<Draggable
|
|
155
|
+
:draggable-id="'pool-item-' + item.id"
|
|
156
|
+
:item="item"
|
|
157
|
+
source-id="pool-main"
|
|
158
|
+
source-kind="pool"
|
|
159
|
+
:source-index="index"
|
|
160
|
+
>
|
|
161
|
+
<template #default="{ dragging }">
|
|
162
|
+
<div
|
|
163
|
+
:style="{
|
|
164
|
+
padding: '6px 10px',
|
|
165
|
+
minWidth: '56px',
|
|
166
|
+
textAlign: 'center',
|
|
167
|
+
borderRadius: '6px',
|
|
168
|
+
border: '1px solid #d1d5db',
|
|
169
|
+
background: '#f9fafb',
|
|
170
|
+
opacity: dragging ? .9 : 1
|
|
171
|
+
}"
|
|
172
|
+
>
|
|
173
|
+
{{ item.label }}
|
|
174
|
+
</div>
|
|
175
|
+
</template>
|
|
176
|
+
</Draggable>
|
|
177
|
+
</template>
|
|
178
|
+
</Pool>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<template #preview="{ item }">
|
|
183
|
+
<div
|
|
184
|
+
v-if="item"
|
|
185
|
+
style="padding: 6px 10px; border-radius: 6px; border: 1px solid #d1d5db; background: #ffffff; box-shadow: 0 4px 12px rgba(0,0,0,0.12);"
|
|
186
|
+
>
|
|
187
|
+
{{ item.label }}
|
|
188
|
+
</div>
|
|
189
|
+
</template>
|
|
190
|
+
</DragAndDrop>
|
|
191
|
+
</div>
|
|
192
|
+
`,
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
198
|
+
import { ref } from 'vue';
|
|
199
|
+
import DragAndDrop from './DragAndDrop.vue';
|
|
200
|
+
import Draggable from './Draggable.vue';
|
|
201
|
+
import Slot from '../Slot/Slot.vue';
|
|
202
|
+
import Pool from '../Pool/Pool.vue';
|
|
203
|
+
import type { PoolReceiveEvent, PoolReorderEvent, SlotDropEvent } from './contracts';
|
|
204
|
+
|
|
205
|
+
interface DemoItem {
|
|
206
|
+
id: string;
|
|
207
|
+
label: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const SLOT_COUNT = 4;
|
|
211
|
+
|
|
212
|
+
const meta: Meta<typeof DragAndDrop> = {
|
|
213
|
+
title: 'Gravity/DragAndDrop',
|
|
214
|
+
component: DragAndDrop,
|
|
215
|
+
tags: ['autodocs'],
|
|
216
|
+
parameters: { layout: 'centered' },
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export default meta;
|
|
220
|
+
type Story = StoryObj<typeof DragAndDrop>;
|
|
221
|
+
|
|
222
|
+
export const Combined: Story = {
|
|
223
|
+
render: () => ({
|
|
224
|
+
setup() {
|
|
225
|
+
const slots = ref<(DemoItem | null)[]>([{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }, null, null]);
|
|
226
|
+
const pool = ref<DemoItem[]>([
|
|
227
|
+
{ id: 'c', label: 'C' },
|
|
228
|
+
{ id: 'd', label: 'D' },
|
|
229
|
+
{ id: 'e', label: 'E' },
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
function takeFromSource(sourceContainerId: string, sourceIndex: number): DemoItem | null {
|
|
233
|
+
if (sourceContainerId.startsWith('slot-')) {
|
|
234
|
+
const slotIndex = Number(sourceContainerId.replace('slot-', ''));
|
|
235
|
+
const moved = slots.value[slotIndex] || null;
|
|
236
|
+
slots.value[slotIndex] = null;
|
|
237
|
+
return moved;
|
|
238
|
+
}
|
|
239
|
+
if (sourceContainerId === 'pool-main') {
|
|
240
|
+
const [moved] = pool.value.splice(sourceIndex, 1);
|
|
241
|
+
return moved || null;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function onSlotDrop(event: SlotDropEvent<unknown>) {
|
|
247
|
+
const slotIndex = Number(event.slotId.replace('slot-', ''));
|
|
248
|
+
if (!Number.isFinite(slotIndex) || slotIndex < 0 || slotIndex >= SLOT_COUNT) return;
|
|
249
|
+
if (event.source.containerId === event.slotId) return;
|
|
250
|
+
if (event.swap && event.source.kind === 'slot') {
|
|
251
|
+
const sourceSlotIndex = Number(event.source.containerId.replace('slot-', ''));
|
|
252
|
+
if (!Number.isFinite(sourceSlotIndex) || sourceSlotIndex < 0 || sourceSlotIndex >= SLOT_COUNT) return;
|
|
253
|
+
const moved = slots.value[sourceSlotIndex];
|
|
254
|
+
if (!moved) return;
|
|
255
|
+
const displaced = slots.value[slotIndex];
|
|
256
|
+
slots.value[slotIndex] = moved;
|
|
257
|
+
slots.value[sourceSlotIndex] = displaced ?? null;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
262
|
+
if (!moved) return;
|
|
263
|
+
const displaced = slots.value[slotIndex];
|
|
264
|
+
slots.value[slotIndex] = moved;
|
|
265
|
+
if (displaced) pool.value.push(displaced);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function onPoolReorder(event: PoolReorderEvent<unknown>) {
|
|
269
|
+
if (event.fromIndex === event.toIndex) return;
|
|
270
|
+
const [moved] = pool.value.splice(event.fromIndex, 1);
|
|
271
|
+
if (!moved) return;
|
|
272
|
+
let insertIndex = event.toIndex;
|
|
273
|
+
if (event.fromIndex < event.toIndex) insertIndex -= 1;
|
|
274
|
+
pool.value.splice(Math.max(0, Math.min(insertIndex, pool.value.length)), 0, moved as DemoItem);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function onPoolReceive(event: PoolReceiveEvent<unknown>) {
|
|
278
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
279
|
+
if (!moved) return;
|
|
280
|
+
const insertIndex = Math.max(0, Math.min(event.insertIndex, pool.value.length));
|
|
281
|
+
pool.value.splice(insertIndex, 0, moved);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
slots,
|
|
286
|
+
pool,
|
|
287
|
+
SLOT_COUNT,
|
|
288
|
+
onSlotDrop,
|
|
289
|
+
onPoolReorder,
|
|
290
|
+
onPoolReceive,
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
components: { DragAndDrop, Draggable, Slot, Pool },
|
|
294
|
+
template: `
|
|
295
|
+
<div style="width: 560px; max-width: 96vw; font-family: system-ui, sans-serif;">
|
|
296
|
+
<DragAndDrop>
|
|
297
|
+
<div style="display: grid; gap: 16px;">
|
|
298
|
+
<div>
|
|
299
|
+
<div style="margin-bottom: 8px; font-size: 14px; font-weight: 600;">Slots</div>
|
|
300
|
+
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;">
|
|
301
|
+
<Slot
|
|
302
|
+
v-for="idx in SLOT_COUNT"
|
|
303
|
+
:key="'slot-' + idx"
|
|
304
|
+
:slot-id="'slot-' + (idx - 1)"
|
|
305
|
+
@drop="onSlotDrop"
|
|
306
|
+
>
|
|
307
|
+
<template #default="{ hovering }">
|
|
308
|
+
<div
|
|
309
|
+
style="min-height: 56px; display: flex; align-items: center; justify-content: center; border-radius: 8px;"
|
|
310
|
+
:style="{ background: hovering ? '#ecfeff' : '#ffffff' }"
|
|
311
|
+
>
|
|
312
|
+
<Draggable
|
|
313
|
+
v-if="slots[idx - 1]"
|
|
314
|
+
:draggable-id="'slot-item-' + (idx - 1)"
|
|
315
|
+
:item="slots[idx - 1]"
|
|
316
|
+
:source-id="'slot-' + (idx - 1)"
|
|
317
|
+
source-kind="slot"
|
|
318
|
+
:source-index="0"
|
|
319
|
+
>
|
|
320
|
+
<template #default="{ dragging }">
|
|
321
|
+
<div
|
|
322
|
+
:style="{
|
|
323
|
+
padding: '6px 10px',
|
|
324
|
+
borderRadius: '6px',
|
|
325
|
+
border: '1px solid #d1d5db',
|
|
326
|
+
background: '#f9fafb',
|
|
327
|
+
opacity: dragging ? 0.3 : 1
|
|
328
|
+
}"
|
|
329
|
+
>
|
|
330
|
+
{{ slots[idx - 1]?.label }}
|
|
331
|
+
</div>
|
|
332
|
+
</template>
|
|
333
|
+
</Draggable>
|
|
334
|
+
<div v-else style="font-size: 12px; color: #9ca3af;">empty</div>
|
|
335
|
+
</div>
|
|
336
|
+
</template>
|
|
337
|
+
</Slot>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div>
|
|
342
|
+
<div style="margin-bottom: 8px; font-size: 14px; font-weight: 600;">Pool</div>
|
|
343
|
+
<Pool
|
|
344
|
+
pool-id="pool-main"
|
|
345
|
+
:items="pool"
|
|
346
|
+
@reorder="onPoolReorder"
|
|
347
|
+
@receive="onPoolReceive"
|
|
348
|
+
>
|
|
349
|
+
<template #item="{ item, index }">
|
|
350
|
+
<Draggable
|
|
351
|
+
:draggable-id="'pool-item-' + item.id"
|
|
352
|
+
:item="item"
|
|
353
|
+
source-id="pool-main"
|
|
354
|
+
source-kind="pool"
|
|
355
|
+
:source-index="index"
|
|
356
|
+
>
|
|
357
|
+
<template #default="{ dragging }">
|
|
358
|
+
<div
|
|
359
|
+
:style="{
|
|
360
|
+
padding: '6px 10px',
|
|
361
|
+
minWidth: '56px',
|
|
362
|
+
textAlign: 'center',
|
|
363
|
+
borderRadius: '6px',
|
|
364
|
+
border: '1px solid #d1d5db',
|
|
365
|
+
background: '#f9fafb',
|
|
366
|
+
opacity: dragging ? 0.3 : 1
|
|
367
|
+
}"
|
|
368
|
+
>
|
|
369
|
+
{{ item.label }}
|
|
370
|
+
</div>
|
|
371
|
+
</template>
|
|
372
|
+
</Draggable>
|
|
373
|
+
</template>
|
|
374
|
+
</Pool>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<template #preview="{ item }">
|
|
379
|
+
<div
|
|
380
|
+
v-if="item"
|
|
381
|
+
style="padding: 6px 10px; border-radius: 6px; border: 1px solid #d1d5db; background: #ffffff; box-shadow: 0 4px 12px rgba(0,0,0,0.12);"
|
|
382
|
+
>
|
|
383
|
+
{{ item.label }}
|
|
384
|
+
</div>
|
|
385
|
+
</template>
|
|
386
|
+
</DragAndDrop>
|
|
387
|
+
</div>
|
|
388
|
+
`,
|
|
389
|
+
}),
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
393
|
+
import { ref } from 'vue';
|
|
394
|
+
import DragAndDrop from './DragAndDrop.vue';
|
|
395
|
+
import Draggable from './Draggable.vue';
|
|
396
|
+
import Slot from '../Slot/Slot.vue';
|
|
397
|
+
import Pool from '../Pool/Pool.vue';
|
|
398
|
+
import type { PoolReceiveEvent, PoolReorderEvent, SlotDropEvent } from './contracts';
|
|
399
|
+
|
|
400
|
+
interface DemoItem {
|
|
401
|
+
id: string;
|
|
402
|
+
label: string;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const SLOT_COUNT = 4;
|
|
406
|
+
|
|
407
|
+
const meta: Meta<typeof DragAndDrop> = {
|
|
408
|
+
title: 'Gravity/DragAndDrop',
|
|
409
|
+
component: DragAndDrop,
|
|
410
|
+
tags: ['autodocs'],
|
|
411
|
+
parameters: { layout: 'centered' },
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
export default meta;
|
|
415
|
+
type Story = StoryObj<typeof DragAndDrop>;
|
|
416
|
+
|
|
417
|
+
export const Combined: Story = {
|
|
418
|
+
render: () => ({
|
|
419
|
+
setup() {
|
|
420
|
+
const slots = ref<(DemoItem | null)[]>([{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }, null, null]);
|
|
421
|
+
const pool = ref<DemoItem[]>([
|
|
422
|
+
{ id: 'c', label: 'C' },
|
|
423
|
+
{ id: 'd', label: 'D' },
|
|
424
|
+
{ id: 'e', label: 'E' },
|
|
425
|
+
]);
|
|
426
|
+
|
|
427
|
+
function takeFromSource(sourceContainerId: string, sourceIndex: number): DemoItem | null {
|
|
428
|
+
if (sourceContainerId.startsWith('slot-')) {
|
|
429
|
+
const slotIndex = Number(sourceContainerId.replace('slot-', ''));
|
|
430
|
+
const moved = slots.value[slotIndex] || null;
|
|
431
|
+
slots.value[slotIndex] = null;
|
|
432
|
+
return moved;
|
|
433
|
+
}
|
|
434
|
+
if (sourceContainerId === 'pool-main') {
|
|
435
|
+
const [moved] = pool.value.splice(sourceIndex, 1);
|
|
436
|
+
return moved || null;
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function onSlotDrop(event: SlotDropEvent<unknown>) {
|
|
442
|
+
const slotIndex = Number(event.slotId.replace('slot-', ''));
|
|
443
|
+
if (!Number.isFinite(slotIndex) || slotIndex < 0 || slotIndex >= SLOT_COUNT) return;
|
|
444
|
+
if (event.source.containerId === event.slotId) return;
|
|
445
|
+
if (event.swap && event.source.kind === 'slot') {
|
|
446
|
+
const sourceSlotIndex = Number(event.source.containerId.replace('slot-', ''));
|
|
447
|
+
if (!Number.isFinite(sourceSlotIndex) || sourceSlotIndex < 0 || sourceSlotIndex >= SLOT_COUNT) return;
|
|
448
|
+
const moved = slots.value[sourceSlotIndex];
|
|
449
|
+
if (!moved) return;
|
|
450
|
+
const displaced = slots.value[slotIndex];
|
|
451
|
+
slots.value[slotIndex] = moved;
|
|
452
|
+
slots.value[sourceSlotIndex] = displaced ?? null;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
457
|
+
if (!moved) return;
|
|
458
|
+
const displaced = slots.value[slotIndex];
|
|
459
|
+
slots.value[slotIndex] = moved;
|
|
460
|
+
if (displaced) pool.value.push(displaced);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function onPoolReorder(event: PoolReorderEvent<unknown>) {
|
|
464
|
+
if (event.fromIndex === event.toIndex) return;
|
|
465
|
+
const [moved] = pool.value.splice(event.fromIndex, 1);
|
|
466
|
+
if (!moved) return;
|
|
467
|
+
let insertIndex = event.toIndex;
|
|
468
|
+
if (event.fromIndex < event.toIndex) insertIndex -= 1;
|
|
469
|
+
pool.value.splice(Math.max(0, Math.min(insertIndex, pool.value.length)), 0, moved as DemoItem);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function onPoolReceive(event: PoolReceiveEvent<unknown>) {
|
|
473
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
474
|
+
if (!moved) return;
|
|
475
|
+
const insertIndex = Math.max(0, Math.min(event.insertIndex, pool.value.length));
|
|
476
|
+
pool.value.splice(insertIndex, 0, moved);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
slots,
|
|
481
|
+
pool,
|
|
482
|
+
SLOT_COUNT,
|
|
483
|
+
onSlotDrop,
|
|
484
|
+
onPoolReorder,
|
|
485
|
+
onPoolReceive,
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
components: { DragAndDrop, Draggable, Slot, Pool },
|
|
489
|
+
template: `
|
|
490
|
+
<div style="width: 560px; max-width: 96vw; font-family: system-ui, sans-serif;">
|
|
491
|
+
<DragAndDrop>
|
|
492
|
+
<div style="display: grid; gap: 16px;">
|
|
493
|
+
<div>
|
|
494
|
+
<div style="margin-bottom: 8px; font-size: 14px; font-weight: 600;">Slots</div>
|
|
495
|
+
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;">
|
|
496
|
+
<Slot
|
|
497
|
+
v-for="idx in SLOT_COUNT"
|
|
498
|
+
:key="'slot-' + idx"
|
|
499
|
+
:slot-id="'slot-' + (idx - 1)"
|
|
500
|
+
@drop="onSlotDrop"
|
|
501
|
+
>
|
|
502
|
+
<template #default="{ hovering }">
|
|
503
|
+
<div
|
|
504
|
+
style="min-height: 56px; display: flex; align-items: center; justify-content: center; border-radius: 8px;"
|
|
505
|
+
:style="{ background: hovering ? '#ecfeff' : '#ffffff' }"
|
|
506
|
+
>
|
|
507
|
+
<Draggable
|
|
508
|
+
v-if="slots[idx - 1]"
|
|
509
|
+
:draggable-id="'slot-item-' + (idx - 1)"
|
|
510
|
+
:item="slots[idx - 1]"
|
|
511
|
+
:source-id="'slot-' + (idx - 1)"
|
|
512
|
+
source-kind="slot"
|
|
513
|
+
:source-index="0"
|
|
514
|
+
>
|
|
515
|
+
<template #default="{ dragging }">
|
|
516
|
+
<div
|
|
517
|
+
:style="{
|
|
518
|
+
padding: '6px 10px',
|
|
519
|
+
borderRadius: '6px',
|
|
520
|
+
border: '1px solid #d1d5db',
|
|
521
|
+
background: '#f9fafb',
|
|
522
|
+
opacity: dragging ? 0.3 : 1
|
|
523
|
+
}"
|
|
524
|
+
>
|
|
525
|
+
{{ slots[idx - 1]?.label }}
|
|
526
|
+
</div>
|
|
527
|
+
</template>
|
|
528
|
+
</Draggable>
|
|
529
|
+
<div v-else style="font-size: 12px; color: #9ca3af;">empty</div>
|
|
530
|
+
</div>
|
|
531
|
+
</template>
|
|
532
|
+
</Slot>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<div>
|
|
537
|
+
<div style="margin-bottom: 8px; font-size: 14px; font-weight: 600;">Pool</div>
|
|
538
|
+
<Pool
|
|
539
|
+
pool-id="pool-main"
|
|
540
|
+
:items="pool"
|
|
541
|
+
@reorder="onPoolReorder"
|
|
542
|
+
@receive="onPoolReceive"
|
|
543
|
+
>
|
|
544
|
+
<template #item="{ item, index }">
|
|
545
|
+
<Draggable
|
|
546
|
+
:draggable-id="'pool-item-' + item.id"
|
|
547
|
+
:item="item"
|
|
548
|
+
source-id="pool-main"
|
|
549
|
+
source-kind="pool"
|
|
550
|
+
:source-index="index"
|
|
551
|
+
>
|
|
552
|
+
<template #default="{ dragging }">
|
|
553
|
+
<div
|
|
554
|
+
:style="{
|
|
555
|
+
padding: '6px 10px',
|
|
556
|
+
minWidth: '56px',
|
|
557
|
+
textAlign: 'center',
|
|
558
|
+
borderRadius: '6px',
|
|
559
|
+
border: '1px solid #d1d5db',
|
|
560
|
+
background: '#f9fafb',
|
|
561
|
+
opacity: dragging ? 0.3 : 1
|
|
562
|
+
}"
|
|
563
|
+
>
|
|
564
|
+
{{ item.label }}
|
|
565
|
+
</div>
|
|
566
|
+
</template>
|
|
567
|
+
</Draggable>
|
|
568
|
+
</template>
|
|
569
|
+
</Pool>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
|
|
573
|
+
<template #preview="{ item }">
|
|
574
|
+
<div
|
|
575
|
+
v-if="item"
|
|
576
|
+
style="padding: 6px 10px; border-radius: 6px; border: 1px solid #d1d5db; background: #ffffff; box-shadow: 0 4px 12px rgba(0,0,0,0.12);"
|
|
577
|
+
>
|
|
578
|
+
{{ item.label }}
|
|
579
|
+
</div>
|
|
580
|
+
</template>
|
|
581
|
+
</DragAndDrop>
|
|
582
|
+
</div>
|
|
583
|
+
`,
|
|
584
|
+
}),
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
588
|
+
import { ref } from 'vue';
|
|
589
|
+
import DragAndDrop from './DragAndDrop.vue';
|
|
590
|
+
import Draggable from './Draggable.vue';
|
|
591
|
+
import Slot from '../Slot/Slot.vue';
|
|
592
|
+
import Pool from '../Pool/Pool.vue';
|
|
593
|
+
import type { PoolReceiveEvent, PoolReorderEvent, SlotDropEvent } from './contracts';
|
|
594
|
+
|
|
595
|
+
interface DemoItem {
|
|
596
|
+
id: string;
|
|
597
|
+
label: string;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const SLOT_COUNT = 4;
|
|
601
|
+
|
|
602
|
+
const meta: Meta<typeof DragAndDrop> = {
|
|
603
|
+
title: 'Gravity/DragAndDrop',
|
|
604
|
+
component: DragAndDrop,
|
|
605
|
+
tags: ['autodocs'],
|
|
606
|
+
parameters: { layout: 'centered' },
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
export default meta;
|
|
610
|
+
type Story = StoryObj<typeof DragAndDrop>;
|
|
611
|
+
|
|
612
|
+
export const Combined: Story = {
|
|
613
|
+
render: () => ({
|
|
614
|
+
setup() {
|
|
615
|
+
const slots = ref<(DemoItem | null)[]>([{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }, null, null]);
|
|
616
|
+
const pool = ref<DemoItem[]>([
|
|
617
|
+
{ id: 'c', label: 'C' },
|
|
618
|
+
{ id: 'd', label: 'D' },
|
|
619
|
+
{ id: 'e', label: 'E' },
|
|
620
|
+
]);
|
|
621
|
+
|
|
622
|
+
function takeFromSource(sourceContainerId: string, sourceIndex: number): DemoItem | null {
|
|
623
|
+
if (sourceContainerId.startsWith('slot-')) {
|
|
624
|
+
const slotIndex = Number(sourceContainerId.replace('slot-', ''));
|
|
625
|
+
const moved = slots.value[slotIndex] || null;
|
|
626
|
+
slots.value[slotIndex] = null;
|
|
627
|
+
return moved;
|
|
628
|
+
}
|
|
629
|
+
if (sourceContainerId === 'pool-main') {
|
|
630
|
+
const [moved] = pool.value.splice(sourceIndex, 1);
|
|
631
|
+
return moved || null;
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function onSlotDrop(event: SlotDropEvent<unknown>) {
|
|
637
|
+
const slotIndex = Number(event.slotId.replace('slot-', ''));
|
|
638
|
+
if (!Number.isFinite(slotIndex) || slotIndex < 0 || slotIndex >= SLOT_COUNT) return;
|
|
639
|
+
if (event.source.containerId === event.slotId) return;
|
|
640
|
+
if (event.swap && event.source.kind === 'slot') {
|
|
641
|
+
const sourceSlotIndex = Number(event.source.containerId.replace('slot-', ''));
|
|
642
|
+
if (!Number.isFinite(sourceSlotIndex) || sourceSlotIndex < 0 || sourceSlotIndex >= SLOT_COUNT) return;
|
|
643
|
+
const moved = slots.value[sourceSlotIndex];
|
|
644
|
+
if (!moved) return;
|
|
645
|
+
const displaced = slots.value[slotIndex];
|
|
646
|
+
slots.value[slotIndex] = moved;
|
|
647
|
+
slots.value[sourceSlotIndex] = displaced ?? null;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
652
|
+
if (!moved) return;
|
|
653
|
+
const displaced = slots.value[slotIndex];
|
|
654
|
+
slots.value[slotIndex] = moved;
|
|
655
|
+
if (displaced) pool.value.push(displaced);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function onPoolReorder(event: PoolReorderEvent<unknown>) {
|
|
659
|
+
if (event.fromIndex === event.toIndex) return;
|
|
660
|
+
const [moved] = pool.value.splice(event.fromIndex, 1);
|
|
661
|
+
if (!moved) return;
|
|
662
|
+
let insertIndex = event.toIndex;
|
|
663
|
+
if (event.fromIndex < event.toIndex) insertIndex -= 1;
|
|
664
|
+
pool.value.splice(Math.max(0, Math.min(insertIndex, pool.value.length)), 0, moved as DemoItem);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function onPoolReceive(event: PoolReceiveEvent<unknown>) {
|
|
668
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
669
|
+
if (!moved) return;
|
|
670
|
+
const insertIndex = Math.max(0, Math.min(event.insertIndex, pool.value.length));
|
|
671
|
+
pool.value.splice(insertIndex, 0, moved);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
slots,
|
|
676
|
+
pool,
|
|
677
|
+
SLOT_COUNT,
|
|
678
|
+
onSlotDrop,
|
|
679
|
+
onPoolReorder,
|
|
680
|
+
onPoolReceive,
|
|
681
|
+
};
|
|
682
|
+
},
|
|
683
|
+
components: { DragAndDrop, Draggable, Slot, Pool },
|
|
684
|
+
template: `
|
|
685
|
+
<div style="width: 560px; max-width: 96vw; font-family: system-ui, sans-serif;">
|
|
686
|
+
<DragAndDrop>
|
|
687
|
+
<div style="display: grid; gap: 16px;">
|
|
688
|
+
<div>
|
|
689
|
+
<div style="margin-bottom: 8px; font-size: 14px; font-weight: 600;">Slots</div>
|
|
690
|
+
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;">
|
|
691
|
+
<Slot
|
|
692
|
+
v-for="idx in SLOT_COUNT"
|
|
693
|
+
:key="'slot-' + idx"
|
|
694
|
+
:slot-id="'slot-' + (idx - 1)"
|
|
695
|
+
@drop="onSlotDrop"
|
|
696
|
+
>
|
|
697
|
+
<template #default="{ hovering }">
|
|
698
|
+
<div
|
|
699
|
+
style="min-height: 56px; display: flex; align-items: center; justify-content: center; border-radius: 8px;"
|
|
700
|
+
:style="{ background: hovering ? '#ecfeff' : '#ffffff' }"
|
|
701
|
+
>
|
|
702
|
+
<Draggable
|
|
703
|
+
v-if="slots[idx - 1]"
|
|
704
|
+
:draggable-id="'slot-item-' + (idx - 1)"
|
|
705
|
+
:item="slots[idx - 1]"
|
|
706
|
+
:source-id="'slot-' + (idx - 1)"
|
|
707
|
+
source-kind="slot"
|
|
708
|
+
:source-index="0"
|
|
709
|
+
>
|
|
710
|
+
<template #default="{ dragging }">
|
|
711
|
+
<div
|
|
712
|
+
:style="{
|
|
713
|
+
padding: '6px 10px',
|
|
714
|
+
borderRadius: '6px',
|
|
715
|
+
border: '1px solid #d1d5db',
|
|
716
|
+
background: '#f9fafb',
|
|
717
|
+
opacity: dragging ? 0.3 : 1
|
|
718
|
+
}"
|
|
719
|
+
>
|
|
720
|
+
{{ slots[idx - 1].label }}
|
|
721
|
+
</div>
|
|
722
|
+
</template>
|
|
723
|
+
</Draggable>
|
|
724
|
+
<div v-else style="font-size: 12px; color: #9ca3af;">empty</div>
|
|
725
|
+
</div>
|
|
726
|
+
</template>
|
|
727
|
+
</Slot>
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
|
|
731
|
+
<div>
|
|
732
|
+
<div style="margin-bottom: 8px; font-size: 14px; font-weight: 600;">Pool</div>
|
|
733
|
+
<Pool
|
|
734
|
+
pool-id="pool-main"
|
|
735
|
+
:items="pool"
|
|
736
|
+
@reorder="onPoolReorder"
|
|
737
|
+
@receive="onPoolReceive"
|
|
738
|
+
>
|
|
739
|
+
<template #item="{ item, index }">
|
|
740
|
+
<Draggable
|
|
741
|
+
:draggable-id="'pool-item-' + item.id"
|
|
742
|
+
:item="item"
|
|
743
|
+
source-id="pool-main"
|
|
744
|
+
source-kind="pool"
|
|
745
|
+
:source-index="index"
|
|
746
|
+
>
|
|
747
|
+
<div
|
|
748
|
+
v-if="showPoolGhostAt(i, dragState.active, dragState.hoverTargetName, dragState.hoverTargetIndex, dragState.source, dragState.index, pool.length)"
|
|
749
|
+
style="height: 34px; min-width: 56px; border-radius: 6px; border: 1px dashed #22c55e; background: #f0fdf4;"
|
|
750
|
+
/>
|
|
751
|
+
|
|
752
|
+
<div
|
|
753
|
+
v-else
|
|
754
|
+
:style="{
|
|
755
|
+
padding: '6px 10px',
|
|
756
|
+
minWidth: '56px',
|
|
757
|
+
textAlign: 'center',
|
|
758
|
+
borderRadius: '6px',
|
|
759
|
+
border: '1px solid #d1d5db',
|
|
760
|
+
background: '#f9fafb',
|
|
761
|
+
cursor: 'grab',
|
|
762
|
+
opacity: isDraggingSource('pool', i) ? 0.3 : 1
|
|
763
|
+
}"
|
|
764
|
+
@pointerdown.prevent="onPointerDown('pool', i, $event)"
|
|
765
|
+
>
|
|
766
|
+
{{ item.label }}
|
|
767
|
+
</div>
|
|
768
|
+
</div>
|
|
769
|
+
</TransitionGroup>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
</template>
|
|
773
|
+
|
|
774
|
+
<template #preview="{ item }">
|
|
775
|
+
<div
|
|
776
|
+
v-if="item"
|
|
777
|
+
style="padding: 6px 10px; border-radius: 6px; border: 1px solid #d1d5db; background: #ffffff; box-shadow: 0 4px 12px rgba(0,0,0,0.12);"
|
|
778
|
+
>
|
|
779
|
+
{{ item.label }}
|
|
780
|
+
</div>
|
|
781
|
+
</template>
|
|
782
|
+
</DragAndDrop>
|
|
783
|
+
</div>
|
|
784
|
+
`,
|
|
785
|
+
}),
|
|
786
|
+
};
|
|
787
|
+
*/
|