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
package/README.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Gravity - Stardust UI's drag-n-drop component.
|
|
2
|
+
|
|
3
|
+
 
|
|
4
|
+
**[>> Check the Demo](https://gravity-dnd.vercel.app/?path=/docs/gravity-librarydemo--docs)**
|
|
5
|
+
|
|
6
|
+
Originally created back in 2019 for [Pollux.gg](https://pollux.gg)'s Medal Picker from the profile
|
|
7
|
+
editor dashboard ([Original source](https://github.com/PolestarLabs/dashboard/blob/96cdfd6a8b49612c807a38230ab909aaf75ca944/src/views/dashboard/pages/profile_edit.pug)). Original was done in pure frontend Vue2
|
|
8
|
+
and Pug using [vuedraggable](https://www.npmjs.com/package/vuedraggable).
|
|
9
|
+
|
|
10
|
+
This package ports it over for more general use as a lightweight, composable
|
|
11
|
+
drag-and-drop system for Vue 3.
|
|
12
|
+
It has been redesigned for use in component libraries and applications where you want
|
|
13
|
+
customizable drop targets, drag sources, and flexible collision behavior.
|
|
14
|
+
|
|
15
|
+
Component structural styles are bundled with the Vue components. Visual color and border treatments are opt-in and come from the package stylesheet.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import 'gravity-dnd/styles.css';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
*Renamed to **Gravity** to keep on-theme with all of other Pollux's side-libs and elements. Mainly its parent component lib: **Stardust UI***
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Core Concepts
|
|
26
|
+
|
|
27
|
+
### GravityProvider
|
|
28
|
+
Wrap any part of your app that uses drag/drop in a single `GravityProvider`.
|
|
29
|
+
It provides context for all draggables, slots, and pools.
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<template>
|
|
33
|
+
<GravityProvider>
|
|
34
|
+
<!-- drag/drop components here -->
|
|
35
|
+
</GravityProvider>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup>
|
|
39
|
+
import 'gravity-dnd/styles.css';
|
|
40
|
+
import { GravityProvider } from '@/ui/gravity';
|
|
41
|
+
</script>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### GravityDraggable
|
|
45
|
+
Represents a draggable item.
|
|
46
|
+
|
|
47
|
+
Key props:
|
|
48
|
+
- `draggable-id` (string): unique id for the drag instance
|
|
49
|
+
- `item` (any): the data payload carried during drag
|
|
50
|
+
- `source-id` (string): identifies the container the drag originates from
|
|
51
|
+
- `source-kind` (`'pool' | 'slot' | 'custom'`): used for identifying what type of container a drag comes from
|
|
52
|
+
- `source-index` (number): index within the container
|
|
53
|
+
- `drop-mode` (`'target' | 'floating'`): controls whether drop is evaluated by hover targets or by pointer release
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<GravityDraggable
|
|
59
|
+
draggable-id="draggable-1"
|
|
60
|
+
:item="item"
|
|
61
|
+
source-id="my-pool"
|
|
62
|
+
source-kind="pool"
|
|
63
|
+
:source-index="index"
|
|
64
|
+
>
|
|
65
|
+
<template #default="{ dragging }">
|
|
66
|
+
<div :style="{ opacity: dragging ? .9 : 1 }">{{ item.label }}</div>
|
|
67
|
+
</template>
|
|
68
|
+
</GravityDraggable>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### GravitySlot
|
|
72
|
+
A drop target that accepts one item at a time.
|
|
73
|
+
It emits `drop` events when an item is dropped.
|
|
74
|
+
|
|
75
|
+
Key props:
|
|
76
|
+
- `slot-id` (string): unique identifier for the slot
|
|
77
|
+
- `item` (any): current item in the slot (optional)
|
|
78
|
+
- `onDropCollision` (`'replace' | 'swap' | 'reject'`): how to handle collisions when slot already contains an item
|
|
79
|
+
- `accepts` (function): optional predicate to allow/reject drops based on item + source
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<GravitySlot
|
|
83
|
+
slot-id="my-slot"
|
|
84
|
+
:item="currentItem"
|
|
85
|
+
onDropCollision="swap"
|
|
86
|
+
:accepts="(item) => item.type === 'allowed'"
|
|
87
|
+
@drop="handleDrop"
|
|
88
|
+
>
|
|
89
|
+
<template #default="{ hovering, accepting }">
|
|
90
|
+
<div :class="{ hover: hovering, accept: accepting }">
|
|
91
|
+
{{ currentItem ? currentItem.label : 'Drop here' }}
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
</GravitySlot>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### GravityPool
|
|
98
|
+
A container that supports reordering items within the pool and receiving items from other sources.
|
|
99
|
+
|
|
100
|
+
Key events:
|
|
101
|
+
- `@reorder` when items inside the pool are reordered
|
|
102
|
+
- `@receive` when an item is dropped into the pool from another source
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<GravityPool pool-id="my-pool" :items="items" @reorder="onReorder" @receive="onReceive">
|
|
106
|
+
<template #item="{ item, index }">
|
|
107
|
+
<GravityDraggable
|
|
108
|
+
:draggable-id="`pool-${item.id}`"
|
|
109
|
+
:item="item"
|
|
110
|
+
source-id="my-pool"
|
|
111
|
+
source-kind="pool"
|
|
112
|
+
:source-index="index"
|
|
113
|
+
>
|
|
114
|
+
<template #default="{ dragging }">
|
|
115
|
+
<div :style="{ opacity: dragging ? .9 : 1 }">{{ item.label }}</div>
|
|
116
|
+
</template>
|
|
117
|
+
</GravityDraggable>
|
|
118
|
+
</template>
|
|
119
|
+
</GravityPool>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Collision Modes (`onDropCollision`)
|
|
125
|
+
|
|
126
|
+
`GravitySlot` supports three collision behaviors when a drop occurs and the slot is already occupied:
|
|
127
|
+
|
|
128
|
+
| Mode | Behavior |
|
|
129
|
+
|---------|----------|
|
|
130
|
+
| `replace` | Overwrites the current slot item with the dropped item. The existing item is discarded. |
|
|
131
|
+
| `swap` | Swaps the dropped item with the existing slot item. The existing item is returned to the drag source. |
|
|
132
|
+
| `reject` | Prevents the drop. The dragged item is returned to its source container. |
|
|
133
|
+
|
|
134
|
+
If `onDropCollision` is not configured, the component uses the older `swap` behavior when `swap=true`, otherwise `replace`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Drop event payload (for slots)
|
|
139
|
+
|
|
140
|
+
Drop handlers receive a `GravitySlotDropEvent<TItem>` with the following shape:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
interface GravitySlotDropEvent<TItem> {
|
|
144
|
+
draggableId: string;
|
|
145
|
+
item: TItem;
|
|
146
|
+
source: {
|
|
147
|
+
containerId: string;
|
|
148
|
+
kind: 'slot' | 'pool' | 'custom';
|
|
149
|
+
index: number;
|
|
150
|
+
};
|
|
151
|
+
target: {
|
|
152
|
+
kind: 'slot' | 'pool' | 'floating';
|
|
153
|
+
containerId: string | null;
|
|
154
|
+
index: number;
|
|
155
|
+
};
|
|
156
|
+
slotId: string;
|
|
157
|
+
swap: boolean;
|
|
158
|
+
collision: 'replace' | 'swap' | 'reject';
|
|
159
|
+
replacedItem?: TItem;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- `collision`: the configured collision rule in effect.
|
|
164
|
+
- `replacedItem`: the item that was in the slot prior to the drop (available for `swap`/`replace`).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Extending / Customizing Behavior
|
|
169
|
+
|
|
170
|
+
### Custom Accept Logic
|
|
171
|
+
Use `accepts` to fine-tune which items can be dropped into a slot.
|
|
172
|
+
|
|
173
|
+
```html
|
|
174
|
+
<GravitySlot
|
|
175
|
+
slot-id="custom-slot"
|
|
176
|
+
:accepts="(item, { sourceContainerId }) => sourceContainerId === 'trusted-pool'"
|
|
177
|
+
@drop="onDrop"
|
|
178
|
+
/>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Custom Slot Rendering
|
|
182
|
+
Use the slot scope values to render feedback:
|
|
183
|
+
|
|
184
|
+
- `hovering`: whether the pointer is hovering the slot
|
|
185
|
+
- `accepting`: whether the current drag is accepted
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<GravitySlot slot-id="styled-slot" @drop="onDrop">
|
|
189
|
+
<template #default="{ hovering, accepting }">
|
|
190
|
+
<div :class="{ 'hovering': hovering, 'accepting': accepting }">
|
|
191
|
+
<!-- custom status UI -->
|
|
192
|
+
</div>
|
|
193
|
+
</template>
|
|
194
|
+
</GravitySlot>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Extending drop logic in parent components
|
|
198
|
+
Use event handlers to implement complex behavior (e.g., persistence, undo, analytics).
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
function handleSlotDrop(event: GravitySlotDropEvent<MyItem>) {
|
|
202
|
+
// apply app-specific rules
|
|
203
|
+
if (event.collision === 'swap') {
|
|
204
|
+
// maybe persist both items
|
|
205
|
+
}
|
|
206
|
+
updateLocalState(event);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Use Cases
|
|
213
|
+
|
|
214
|
+
### 1) Drag-to-slot with swap behavior
|
|
215
|
+
- Use case: a single slot that accepts an item, but dropping another item should return the original back to its source.
|
|
216
|
+
- Configure:
|
|
217
|
+
- `onDropCollision="swap"`
|
|
218
|
+
|
|
219
|
+
### 2) Drag-to-slot with replace behavior
|
|
220
|
+
- Use case: a slot that always accepts the newest item and discards the previous.
|
|
221
|
+
- Configure:
|
|
222
|
+
- `onDropCollision="replace"` (or `swap=false`)
|
|
223
|
+
|
|
224
|
+
### 3) Drop rejection based on slot fullness
|
|
225
|
+
- Use case: only allow a drop when the slot is empty.
|
|
226
|
+
- Configure:
|
|
227
|
+
- `onDropCollision="reject"`
|
|
228
|
+
|
|
229
|
+
### 4) Ordered pool with receive/reorder
|
|
230
|
+
- Use case: a list of draggable items where items can be reordered and new items can be dropped in.
|
|
231
|
+
- Use `GravityPool` + `@reorder` + `@receive`.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Quick Start (smallest sample)
|
|
236
|
+
|
|
237
|
+
```html
|
|
238
|
+
<template>
|
|
239
|
+
<GravityProvider>
|
|
240
|
+
<GravitySlot slot-id="target" onDropCollision="swap" @drop="onDrop">
|
|
241
|
+
<template #default="{ hovering, accepting }">
|
|
242
|
+
<div :style="{ background: hovering ? (accepting ? '#e0f7ff' : '#ffeaea') : '#fff' }">
|
|
243
|
+
Drop an item here
|
|
244
|
+
</div>
|
|
245
|
+
</template>
|
|
246
|
+
</GravitySlot>
|
|
247
|
+
|
|
248
|
+
<GravityPool pool-id="pool" :items="items" @receive="onReceive" @reorder="onReorder">
|
|
249
|
+
<template #item="{ item, index }">
|
|
250
|
+
<GravityDraggable
|
|
251
|
+
:draggable-id="`item-${item.id}`"
|
|
252
|
+
:item="item"
|
|
253
|
+
source-id="pool"
|
|
254
|
+
source-kind="pool"
|
|
255
|
+
:source-index="index"
|
|
256
|
+
>
|
|
257
|
+
<template #default="{ dragging }">
|
|
258
|
+
<div :style="{ opacity: dragging ? 0.3 : 1 }">{{ item.label }}</div>
|
|
259
|
+
</template>
|
|
260
|
+
</GravityDraggable>
|
|
261
|
+
</template>
|
|
262
|
+
</GravityPool>
|
|
263
|
+
</GravityProvider>
|
|
264
|
+
</template>
|
|
265
|
+
|
|
266
|
+
<script setup lang="ts">
|
|
267
|
+
import 'gravity-dnd/styles.css';
|
|
268
|
+
import { ref } from 'vue';
|
|
269
|
+
import { GravityProvider, GravityPool, GravitySlot, GravityDraggable } from '@/ui/gravity';
|
|
270
|
+
|
|
271
|
+
const items = ref([{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }]);
|
|
272
|
+
|
|
273
|
+
function onDrop(event) {
|
|
274
|
+
// handle drop
|
|
275
|
+
}
|
|
276
|
+
function onReceive(event) {
|
|
277
|
+
// handle received item
|
|
278
|
+
}
|
|
279
|
+
function onReorder(event) {
|
|
280
|
+
// handle reorder
|
|
281
|
+
}
|
|
282
|
+
</script>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Notes
|
|
288
|
+
- This drag/drop system is intentionally lightweight and does not have built-in keyboard accessibility.
|
|
289
|
+
- For production use, wrap drag-drop logic in safe state updates (avoid mutating arrays directly in complex UIs).
|
|
290
|
+
- The `onDropCollision` API is designed for specific use-cases from Pollux.gg's dashboard, its implementation might be a bit stiff.
|
|
291
|
+
- Better mobile support is planned for future iterations
|
|
292
|
+
- React support is being considered.
|
package/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { default as GravityProvider } from './src/components/Draggable/DragDropProvider.vue';
|
|
2
|
+
export { default as GravityDraggable } from './src/components/Draggable/Draggable.vue';
|
|
3
|
+
export { default as GravitySlot } from './src/components/Slot/Slot.vue';
|
|
4
|
+
export { default as GravityPool } from './src/components/Pool/Pool.vue';
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
GravityBoundary,
|
|
8
|
+
GravityCanDragContext,
|
|
9
|
+
GravityContainerKind,
|
|
10
|
+
GravityDropEvent,
|
|
11
|
+
GravityDropTarget,
|
|
12
|
+
GravityDraggableDropEvent,
|
|
13
|
+
GravityHoverTarget,
|
|
14
|
+
GravityMode,
|
|
15
|
+
GravityPoolReceiveEvent,
|
|
16
|
+
GravityPoolReorderEvent,
|
|
17
|
+
GravitySlotDropEvent,
|
|
18
|
+
GravitySource,
|
|
19
|
+
} from './src/components/Draggable/contracts';
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gravity-dnd",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "1.1.6",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"authors": [
|
|
7
|
+
{
|
|
8
|
+
"name": "L Reis / Flicky / Andromika",
|
|
9
|
+
"email": "github@amarok.com"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "Polestar Labs",
|
|
13
|
+
"email": "hello@pollux.gg"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"description": "A Vue 3 drag-and-drop component library.",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./index.ts",
|
|
19
|
+
"./styles.css": "./styles.css",
|
|
20
|
+
"./styles.scss": "./styles.scss"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "npm run storybook -- --ci --port 6007",
|
|
24
|
+
"build": "npm run build:storybook",
|
|
25
|
+
"build-storybook": "npm run build:storybook",
|
|
26
|
+
"storybook": "storybook dev -p 6007 --ci",
|
|
27
|
+
"build:storybook": "storybook build --output-dir dist",
|
|
28
|
+
"lint": "eslint . --ext .ts,.tsx,.vue",
|
|
29
|
+
"preview": "vite preview"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"vue",
|
|
33
|
+
"dnd",
|
|
34
|
+
"drag-and-drop",
|
|
35
|
+
"component-library"
|
|
36
|
+
],
|
|
37
|
+
"author": "",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"vue": "^3.4.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@storybook/addon-a11y": "^10.2.19",
|
|
44
|
+
"@storybook/addon-docs": "^10.2.19",
|
|
45
|
+
"@storybook/builder-vite": "^10.2.19",
|
|
46
|
+
"@storybook/vue3-vite": "^10.2.19",
|
|
47
|
+
"@vitejs/plugin-vue": "^6.0.5",
|
|
48
|
+
"@vue/compiler-sfc": "^3.4.0",
|
|
49
|
+
"chromatic": "^15.3.0",
|
|
50
|
+
"eslint": "^8.0.0",
|
|
51
|
+
"eslint-plugin-storybook": "10.2.19",
|
|
52
|
+
"eslint-plugin-vue": "^9.0.0",
|
|
53
|
+
"prismjs": "^1.29.0",
|
|
54
|
+
"pug": "^3.0.4",
|
|
55
|
+
"pug-plain-loader": "^1.1.0",
|
|
56
|
+
"react": "^18.3.1",
|
|
57
|
+
"react-dom": "^18.3.1",
|
|
58
|
+
"sass-embedded": "^1.98.0",
|
|
59
|
+
"storybook": "^10.2.19",
|
|
60
|
+
"typescript": "^5.0.0",
|
|
61
|
+
"vite": "^8.0.0",
|
|
62
|
+
"vite-plugin-dts": "^4.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/public/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import docs from '../README.md?raw';
|
|
4
|
+
import {
|
|
5
|
+
GravityDraggable,
|
|
6
|
+
GravityPool,
|
|
7
|
+
GravityProvider,
|
|
8
|
+
GravitySlot,
|
|
9
|
+
type GravityPoolReceiveEvent,
|
|
10
|
+
type GravityPoolReorderEvent,
|
|
11
|
+
type GravitySlotDropEvent,
|
|
12
|
+
} from '../index';
|
|
13
|
+
|
|
14
|
+
interface DemoItem {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const meta: Meta = {
|
|
20
|
+
title: 'Gravity/LibraryDemo',
|
|
21
|
+
tags: ['autodocs'],
|
|
22
|
+
parameters: {
|
|
23
|
+
layout: 'centered',
|
|
24
|
+
docs: {
|
|
25
|
+
description: {
|
|
26
|
+
component: docs,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof meta>;
|
|
34
|
+
|
|
35
|
+
export const Playground: Story = {
|
|
36
|
+
render: () => ({
|
|
37
|
+
setup() {
|
|
38
|
+
const loose = ref<DemoItem | null>({ id: 'loose', label: 'Loose Token' });
|
|
39
|
+
const slotItem = ref<DemoItem | null>(null);
|
|
40
|
+
const pool = ref<DemoItem[]>([
|
|
41
|
+
{ id: 'pool-a', label: 'A' },
|
|
42
|
+
{ id: 'pool-b', label: 'B' },
|
|
43
|
+
{ id: 'pool-c', label: 'C' },
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function takeFromSource(sourceContainerId: string, sourceIndex: number): DemoItem | null {
|
|
47
|
+
if (sourceContainerId === 'pool-main') {
|
|
48
|
+
const [moved] = pool.value.splice(sourceIndex, 1);
|
|
49
|
+
return moved || null;
|
|
50
|
+
}
|
|
51
|
+
if (sourceContainerId === 'slot-main') {
|
|
52
|
+
const moved = slotItem.value;
|
|
53
|
+
slotItem.value = null;
|
|
54
|
+
return moved;
|
|
55
|
+
}
|
|
56
|
+
if (sourceContainerId === 'loose-main') {
|
|
57
|
+
const moved = loose.value;
|
|
58
|
+
loose.value = null;
|
|
59
|
+
return moved;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function putIntoSource(sourceContainerId: string, sourceIndex: number, item: DemoItem) {
|
|
65
|
+
if (sourceContainerId === 'pool-main') {
|
|
66
|
+
const insertIndex = Math.max(0, Math.min(sourceIndex, pool.value.length));
|
|
67
|
+
pool.value.splice(insertIndex, 0, item);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (sourceContainerId === 'slot-main') {
|
|
71
|
+
slotItem.value = item;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (sourceContainerId === 'loose-main') {
|
|
75
|
+
loose.value = item;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onSlotDrop(event: GravitySlotDropEvent<unknown>) {
|
|
81
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
82
|
+
if (!moved) return;
|
|
83
|
+
|
|
84
|
+
// Handle collision rules (replace / swap / reject)
|
|
85
|
+
if (event.collision === 'reject') {
|
|
86
|
+
// Put the moved item back into the source
|
|
87
|
+
putIntoSource(event.source.containerId, event.source.index, moved);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const existing = slotItem.value;
|
|
92
|
+
slotItem.value = moved;
|
|
93
|
+
|
|
94
|
+
if (event.collision === 'swap' && event.replacedItem) {
|
|
95
|
+
putIntoSource(event.source.containerId, event.source.index, event.replacedItem as DemoItem);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function onPoolReorder(event: GravityPoolReorderEvent<unknown>) {
|
|
100
|
+
if (event.fromIndex === event.toIndex) return;
|
|
101
|
+
const [moved] = pool.value.splice(event.fromIndex, 1);
|
|
102
|
+
if (!moved) return;
|
|
103
|
+
let insertIndex = event.toIndex;
|
|
104
|
+
if (event.fromIndex < event.toIndex) insertIndex -= 1;
|
|
105
|
+
pool.value.splice(Math.max(0, Math.min(insertIndex, pool.value.length)), 0, moved as DemoItem);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onPoolReceive(event: GravityPoolReceiveEvent<unknown>) {
|
|
109
|
+
const moved = takeFromSource(event.source.containerId, event.source.index);
|
|
110
|
+
if (!moved) return;
|
|
111
|
+
const insertIndex = Math.max(0, Math.min(event.insertIndex, pool.value.length));
|
|
112
|
+
pool.value.splice(insertIndex, 0, moved);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
loose,
|
|
117
|
+
slotItem,
|
|
118
|
+
pool,
|
|
119
|
+
onSlotDrop,
|
|
120
|
+
onPoolReorder,
|
|
121
|
+
onPoolReceive,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
components: { GravityProvider, GravityDraggable, GravitySlot, GravityPool },
|
|
125
|
+
template: `
|
|
126
|
+
<div style="width: 640px; max-width: 96vw; font-family: system-ui, sans-serif;">
|
|
127
|
+
<GravityProvider>
|
|
128
|
+
<div style="display: grid; gap: 16px;">
|
|
129
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
|
130
|
+
<div>
|
|
131
|
+
<div style="font-size: 13px; font-weight: 600; margin-bottom: 8px;">Loose Draggable</div>
|
|
132
|
+
<GravityDraggable
|
|
133
|
+
v-if="loose"
|
|
134
|
+
draggable-id="gravity-loose-item"
|
|
135
|
+
:item="loose"
|
|
136
|
+
source-id="loose-main"
|
|
137
|
+
source-kind="custom"
|
|
138
|
+
:source-index="0"
|
|
139
|
+
>
|
|
140
|
+
<template #default="{ dragging }">
|
|
141
|
+
<div
|
|
142
|
+
:style="{
|
|
143
|
+
display: 'inline-flex',
|
|
144
|
+
padding: '8px 12px',
|
|
145
|
+
borderRadius: '8px',
|
|
146
|
+
border: '1px solid #d1d5db',
|
|
147
|
+
background: '#f9fafb',
|
|
148
|
+
opacity: dragging ? .9 : 1
|
|
149
|
+
}"
|
|
150
|
+
>
|
|
151
|
+
{{ loose.label }}
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
154
|
+
</GravityDraggable>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div>
|
|
158
|
+
<div style="font-size: 13px; font-weight: 600; margin-bottom: 8px;">Slot</div>
|
|
159
|
+
<GravitySlot slot-id="slot-main" :item="slotItem" onDropCollision="swap" @drop="onSlotDrop">
|
|
160
|
+
<template #default="{ hovering, accepting }">
|
|
161
|
+
<div
|
|
162
|
+
style="min-height: 54px; border-radius: 8px; border: 1px solid #d1d5db; display: flex; align-items: center; justify-content: center;"
|
|
163
|
+
:style="{ background: hovering ? (accepting ? '#ecfeff' : '#fff1f2') : '#fff' }"
|
|
164
|
+
>
|
|
165
|
+
{{ slotItem ? slotItem.label : 'Drop here' }}
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
</GravitySlot>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div>
|
|
173
|
+
<div style="font-size: 13px; font-weight: 600; margin-bottom: 8px;">Pool (reorder + receive)</div>
|
|
174
|
+
<GravityPool pool-id="pool-main" :items="pool" @reorder="onPoolReorder" @receive="onPoolReceive">
|
|
175
|
+
<template #item="{ item, index }">
|
|
176
|
+
<GravityDraggable
|
|
177
|
+
:draggable-id="'gravity-pool-item-' + item.id"
|
|
178
|
+
:item="item"
|
|
179
|
+
source-id="pool-main"
|
|
180
|
+
source-kind="pool"
|
|
181
|
+
:source-index="index"
|
|
182
|
+
>
|
|
183
|
+
<template #default="{ dragging }">
|
|
184
|
+
<div
|
|
185
|
+
:style="{
|
|
186
|
+
padding: '6px 10px',
|
|
187
|
+
minWidth: '56px',
|
|
188
|
+
textAlign: 'center',
|
|
189
|
+
borderRadius: '6px',
|
|
190
|
+
border: '1px solid #d1d5db',
|
|
191
|
+
background: '#f9fafb',
|
|
192
|
+
opacity: dragging ? .9 : 1
|
|
193
|
+
}"
|
|
194
|
+
>
|
|
195
|
+
{{ item.label }}
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
198
|
+
</GravityDraggable>
|
|
199
|
+
</template>
|
|
200
|
+
</GravityPool>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</GravityProvider>
|
|
204
|
+
</div>
|
|
205
|
+
`,
|
|
206
|
+
}),
|
|
207
|
+
};
|